Modern C++ (C++11 to C++20)
“Modern C++” refers to the massive evolution of the language starting with C++11. It emphasizes safety, expressiveness, and performance. If you are writing C++ like it’s 1998, you are working too hard.
1. Type Inference (auto)
Let the compiler deduce types for you. It prevents typos, makes refactoring easier, and avoids accidental type mismatches that cause silent performance bugs.
When NOT to use auto: If the deduced type is not obvious from the right-hand side, spell it out. auto x = getResult() is unclear — is it an int? A string? A shared_ptr? Write int x = getResult() when the type matters for understanding the code. The “Almost Always Auto” style has vocal proponents, but readability should be the deciding factor.
2. Smart Pointers (Memory Safety)
We covered this in the Memory chapter, but it bears repeating: Never use new and delete.
std::unique_ptr: The default choice. Fast, safe, exclusive ownership.
std::shared_ptr: Use only when ownership is truly shared.
Before C++11, returning large objects (like a vector with 1M items) involved copying the entire object — allocating new memory, copying every element, then destroying the original. This was painfully slow and led to convoluted code patterns to avoid returning by value.
Move Semantics allow resources to be “stolen” from temporary objects. The analogy: imagine you are moving to a new apartment. Copying is buying identical furniture for the new place and throwing away the old furniture. Moving is loading your existing furniture onto a truck and driving it over. The result is the same — furnished apartment — but moving is dramatically cheaper.
Under the hood, a move operation for a std::vector copies three values (pointer, size, capacity) and sets the source to null. That is O(1) regardless of how many elements the vector holds.
When to use std::move explicitly
The compiler automatically moves from temporaries (rvalues). You need std::move to move from a named variable — it tells the compiler “I’m done with this, you can steal from it.”
Do not over-use std::move. Moving from a variable and then using it is a bug. Also, do not write return std::move(localVar); — this actually prevents the compiler’s Return Value Optimization (RVO), which is even better than a move. Just write return localVar; and let the compiler optimize.
4. Structured Bindings (C++17)
Allows you to unpack tuples, pairs, and structs directly into variables. It’s similar to destructuring in JavaScript/Python.
5. std::optional (C++17)
Express that a value might be missing without using “magic numbers” (like returning -1 for “not found”) or null pointers. Before optional, C++ code was riddled with conventions like “return an empty string on failure” or “return -1 if the index doesn’t exist.” These are error-prone because the caller might forget to check. std::optional makes the “might be empty” part of the type itself, so you cannot ignore it.
Practical tip: Use optional for function return values that might legitimately have no answer (database lookups, config parsing, search operations). Do not use it for error handling where you need to know why something failed — use exceptions or std::expected (C++23) for that.
6. std::variant (C++17)
A type-safe union. It can hold one of several predefined types, but only one at a time. Unlike C-style union, it tracks which type is currently active and prevents you from reading the wrong one. It is useful for state machines, parsers, AST nodes, and anywhere you have a “this OR that” situation.
The Visitor Pattern with std::visit
The real power of variant comes from std::visit, which lets you handle each possible type cleanly without chains of if/else or std::get calls.
7. Concepts (C++20)
Concepts allow you to constrain template parameters. Before C++20, if you passed the wrong type to a template, you got an incomprehensible error message — sometimes hundreds of lines long, referencing internal implementation details. Concepts fix this by letting you state your requirements upfront.
Think of concepts as a “requirements checklist” for types. Instead of saying “this function takes any type T” and hoping for the best, you say “this function takes any type T that supports addition and comparison.”
Practical impact: Concepts make templates accessible to non-experts. Before C++20, writing template-heavy code was an advanced skill partly because the error messages were so difficult to parse. With concepts, template errors read like English. If you are writing library code or reusable components, concepts should be your default tool for constraining templates.
8. Ranges (C++20)
Ranges allow you to compose algorithms using the pipe operator (|). If you have used Unix shell pipelines (cat file | grep pattern | sort), ranges will feel familiar. Each step transforms the data and passes it to the next step. The key insight: ranges are lazy — they do not create intermediate containers. The filtering, transforming, and taking all happen in a single pass as you iterate.
Compare this to the pre-ranges equivalent, which requires multiple loops or temporary containers:
Ranges make the intent of the code clear while being equally efficient.
The Modern C++ Mindset
Adopting modern C++ is not just about learning new syntax — it is a shift in how you think about writing code:
- Express intent, not mechanism. Use
auto, ranges, and structured bindings so your code reads like a description of what it does, not how it does it.
- Make illegal states unrepresentable. Use
std::optional instead of magic sentinel values. Use std::variant instead of type-unsafe unions. Use enum class instead of raw integers.
- Ownership is a design decision. Every resource in your program should have a clear owner.
unique_ptr for single ownership, shared_ptr for shared ownership, and stack allocation for everything else.
- Zero-cost abstractions are real. Move semantics,
constexpr, and templates compile down to the same machine code you would write by hand. There is no reason to avoid them for “performance.”
Summary
Modern C++ is a different beast from “C with Classes”.
- Safety: Smart pointers and
std::optional eliminate common bugs.
- Expressiveness:
auto, structured bindings, and ranges make code readable.
- Performance: Move semantics ensure abstractions are zero-cost.
- Clarity: Concepts give you readable error messages and self-documenting template interfaces.
You are now equipped with the knowledge to write professional, modern C++.