Modern Features
By the end of this lesson you'll write modern, idiomatic C++: let the compiler deduce types with auto , loop cleanly with range-based for , pass behaviour around with lambdas, model "maybe a value" with std::optional , unpack data with structured bindings, and compute at compile time with constexpr .
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
💡 Real-World Analogy
Think of "old" C++ as filling in a paper form where you must hand-write the same details in every box. Modern C++ is the smart online form: auto auto-fills the type for you, range-based for reads every row without you tracking line numbers, and std::optional is a field that's clearly marked "may be left blank" instead of you writing -1 and hoping the next reader knows it means "empty". The language does the bookkeeping so you can focus on the meaning.
🧭 A Quick Map of the Standards
Standard
Headline features
C++11
auto , range-based for , lambdas, nullptr , brace init
C++14
generic lambdas, relaxed constexpr
C++17
structured bindings, std::optional , if / switch with initialiser
C++20
concepts, ranges, constexpr almost everywhere
You select a standard with a compiler flag, e.g. -std=c++17 or -std=c++20 . Our runner uses a modern standard, so every example below runs as written.
1. auto , Range-Based for , nullptr & Brace Init
auto tells the compiler "work out the type from the value" — it's still fully static, just less typing. Range-based for walks every element without you managing an index; reach for const auto& by default so you read each element with no copying. nullptr is the type-safe "no pointer" (never use 0 or NULL any more), and brace initialisation with { } works for everything and blocks silent narrowing. Read the worked example, run it, then you'll write your own.
Your turn. The program below averages some temperatures — fill in the two blanks marked ___ using the hints, then run it.
2. Lambdas, Structured Bindings & std::optional
A lambda is a function you can write inline and store in a variable: [capture](args) { body } . The capture list decides which outside variables it can see — [x] copies x (by value), [&x] shares it (by reference). A structured binding , auto [a, b] = pair; , unpacks a pair, tuple, or struct into named pieces in one line. And std::optional<T> models "a T that might be missing" — check it with .has_value() and read it safely with .value_or(fallback) . The if (init; cond) form (C++17) lets you declare and test in a single, tightly-scoped line.
Now you try. Split a pair with a structured binding, then guard an optional before reading it. Fill in the two blanks:
3. constexpr , switch with Initialiser & a C++20 Peek
constexpr marks something the compiler can compute before the program runs — so it costs nothing at run time and can do things ordinary values can't, like sizing an array. The switch (init; value) form mirrors the if initialiser, keeping a helper variable scoped to just that block. Finally, a brief look at C++20 : concepts name a requirement a template type must meet (clearer errors than the old SFINAE tricks), and ranges let you pipe algorithms together with | .
Pro Tips
- 💡 Default to const auto& in range-based loops — no copies, and you can't accidentally mutate the source.
- 💡 Capture lambdas by value if the lambda may outlive the variables; [&] is for short-lived, local use only.
- 💡 Return std::optional from functions that might find nothing — it documents "may be empty" far better than a magic -1 .
- 💡 Prefer brace init { } — it works everywhere and refuses to silently truncate (narrow) your data.
Common Errors (and the fix)
- Dangling lambda capture by reference: a lambda using [&] reads a local that's already gone → undefined behaviour / crash. If the lambda outlives the variable, capture by value ( [=] or [x] ) so it owns a copy.
- auto drops const and & : auto x = ref; makes a copy , not a reference, and strips const . When you want a reference, write it explicitly: const auto& x = ref; .
- "terminate called after throwing bad_optional_access ": you called .value() on an empty optional . Check .has_value() first, or use .value_or(fallback) .
- "narrowing conversion ... inside { } ": brace init refuses to truncate, e.g. int n { 3.9 } ; won't compile. Use the right type ( double n { 3.9 } ; ) or cast deliberately with int n { static_cast<int>(3.9) } ; .
📋 Quick Reference (feature → version)
Feature
Syntax
Since
Type deduction
auto x = 42;
Range-based for
for (const auto& e : v)
Lambda
[x](int n){ return n+x; }
Null pointer
int* p = nullptr;
Brace init
vector<int> v{1,2,3};
Structured binding
auto [a, b] = pair;
Optional
optional<int> o = nullopt;
if / switch init
if (auto r = f(); r) ...
constexpr fn
constexpr int sq(int x)
C++11/14
Concepts / ranges
template<Number T>
Frequently Asked Questions
Mini-Challenge: Cheapest Item Finder
No blanks this time — just a brief and an outline to keep you on track. Combine everything: a vector of pairs, a function returning std::optional , a range-based for , and structured bindings. Build it, run it, and check your output against the example in the comments.
🎉 Lesson Complete
- ✅ auto deduces a single static type — less typing, same safety
- ✅ Range-based for with const auto& reads elements with no copies
- ✅ Lambdas capture by value [x] (copy) or by reference [&x] (shared)
- ✅ nullptr , structured bindings, and brace init { } are the modern defaults
- ✅ std::optional models "maybe a value" — check .has_value() / use .value_or()
- ✅ if / switch initialisers and constexpr tighten scope and move work to compile time
- ✅ Next lesson: Operator Overloading — give your own types natural + , == , and << behaviour
Practice quiz
What is true about auto x = 5; in C++?
- x can later hold text, like in JavaScript
- x is deduced as int at compile time and stays int
- auto defers the type to run time
- auto makes x a reference to 5
Answer: x is deduced as int at compile time and stays int. auto is fully static — the compiler deduces one fixed type (here int) at compile time; it never changes.
In a range-based for loop, why is const auto& the safe default?
- It copies each element for safety
- It reads each element with no copy and can't mutate the source
- It is required for the loop to compile
- It converts every element to a string
Answer: It reads each element with no copy and can't mutate the source. const auto& binds a read-only reference, so there is no copying and you cannot accidentally change the container.
A lambda captures a local by reference with [&] and is then used after that local goes out of scope. What happens?
- It safely reads a copy of the value
- A compile error
- Undefined behaviour from a dangling reference
- The lambda recreates the variable
Answer: Undefined behaviour from a dangling reference. [&] captures by reference; using the lambda after the referenced variable dies is a dangling reference — undefined behaviour. Capture by value if the lambda outlives the variable.
What does std::optional<T> model?
- A T that might be missing, checkable with has_value()
- A pointer that is always non-null
- A T that is computed lazily on first use
- A thread-safe wrapper around T
Answer: A T that might be missing, checkable with has_value(). optional<T> represents 'maybe a value' — far clearer than a magic -1; check with has_value() or read safely with value_or(fallback).
What does calling .value() on an empty std::optional do?
- Returns 0
- Returns a default-constructed T
- Throws std::bad_optional_access
- Returns nullptr
Answer: Throws std::bad_optional_access. Reading an empty optional with .value() throws bad_optional_access; guard with has_value() or use value_or().
What does a structured binding like auto [name, level] = player; do?
- Creates two pointers into player
- Unpacks a pair/tuple/struct into named pieces in one line
- Sorts the members of player
- Only works on std::array
Answer: Unpacks a pair/tuple/struct into named pieces in one line. Structured bindings (C++17) split a pair, tuple, or struct into named variables in a single declaration.
How does constexpr differ from const?
- They are identical
- const must be compile-time; constexpr may be run-time
- constexpr must be computable at compile time; const may be computed at run time
- constexpr only applies to pointers
Answer: constexpr must be computable at compile time; const may be computed at run time. const means 'not changeable after set' (possibly run-time); constexpr is stronger — the value must be known at compile time, so it can size arrays.
Given constexpr int square(int x){ return x*x; }, what is the value of square(4) used to size an array?
- 8
- 16
- 4
- It cannot size an array
Answer: 16.
Continue this course
- Previous: Constexpr & Compile-Time Programming
- Next: Operator Overloading