Lesson 23 • Advanced
Modern C++ Features (11/14/17/20)
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.
What You'll Learn
- Use auto and type deduction to drop redundant type names
- Loop with range-based for (and why const auto& is the safe default)
- Write lambdas and choose capture by value vs by reference
- Use nullptr, structured bindings, and uniform brace init {}
- Model missing values with std::optional and check before use
- Use if/switch with an initialiser and constexpr; meet C++20 concepts & ranges
vector, and basic templates. If constexpr is new, peek at Constexpr & Compile-Time Programming first — everything else we build here.💡 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.
Worked example: auto, range-for, nullptr, {} init
Read every comment, run it, and check the output matches.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main() {
// 'auto' asks the compiler to deduce the type from the value.
auto count = 42; // deduced as int
auto price = 9.99; // deduced as double
auto name = string("Sam");// deduced as std::string
// Uniform / brace initialisation — {} works for everything
// and BLOCKS narrowing (see Common Errors below).
vector<int> scores{90, 75, 88, 100}; // a list of ints
/
...Your turn. The program below averages some temperatures — fill in the two blanks marked ___ using the hints, then run it.
🎯 Your turn: auto + range-based for
Fill in the ___ blanks, then check your output against the expected line.
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 🎯 YOUR TURN — replace each ___ then press "Run".
// 1) Use auto to let the compiler deduce the type of this list.
___ temps = vector<double>{19.5, 21.0, 18.2}; // 👉 write: auto
double total = 0.0;
// 2) Loop over every element by reference (read-only).
for (const auto& ___ : temps) { // 👉 name the loop variable, e.g. t
total += t; // (uses the name you chose: t)
...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.
Worked example: lambdas, bindings, optional, if-init
See capture-by-value vs by-reference, and a safe optional check.
#include <iostream>
#include <vector>
#include <algorithm>
#include <optional>
#include <string>
using namespace std;
// optional<T> = "a T that might be missing". No magic -1 needed.
optional<int> findScore(const vector<pair<string,int>>& book, const string& who) {
for (const auto& [person, score] : book) { // structured binding!
if (person == who) return score; // found it
}
return nullopt; // not found -> empty optional
}
int main() {
vector<pair<string,int>
...Now you try. Split a pair with a structured binding, then guard an optional before reading it. Fill in the two blanks:
🎯 Your turn: structured binding + optional check
Bind the pair, then call the method that reports whether a value exists.
#include <iostream>
#include <optional>
#include <string>
using namespace std;
// Returns the half of an even number, or nothing for an odd number.
optional<int> halfIfEven(int n) {
if (n % 2 == 0) return n / 2;
return nullopt;
}
int main() {
// 🎯 YOUR TURN — fill in the two blanks.
// 1) A pair, then split it with a STRUCTURED BINDING.
pair<string,int> player{"Zoe", 3};
auto [name, level] = ___; // 👉 bind to: player
cout << name << " is on level " << lev
...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 |.
Worked example: constexpr, switch-init, concepts & ranges peek
Compile-time computation plus a taste of C++20.
#include <iostream>
#include <string>
using namespace std;
// constexpr = computed at COMPILE time. Zero run-time cost, and the
// result can size arrays or feed templates.
constexpr int square(int x) { return x * x; }
int main() {
constexpr int side = 4;
constexpr int area = square(side); // computed before the program runs
int grid[area]; // legal: 'area' is a compile-time 16
cout << "Grid cells: " << sizeof(grid) / sizeof(int) << "\n"; // 16
// 'swi
...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::optionalfrom 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. autodropsconstand&:auto x = ref;makes a copy, not a reference, and stripsconst. When you want a reference, write it explicitly:const auto& x = ref;.- "terminate called after throwing
bad_optional_access": you called.value()on an emptyoptional. 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 withint n{static_cast<int>(3.9)};.
📋 Quick Reference (feature → version)
| Feature | Syntax | Since |
|---|---|---|
| Type deduction | auto x = 42; | C++11 |
| Range-based for | for (const auto& e : v) | C++11 |
| Lambda | [x](int n){ return n+x; } | C++11 |
| Null pointer | int* p = nullptr; | C++11 |
| Brace init | vector<int> v{1,2,3}; | C++11 |
| Structured binding | auto [a, b] = pair; | C++17 |
| Optional | optional<int> o = nullopt; | C++17 |
| if / switch init | if (auto r = f(); r) ... | C++17 |
| constexpr fn | constexpr int sq(int x) | C++11/14 |
| Concepts / ranges | template<Number T> | C++20 |
Frequently Asked Questions
Q: Is auto the same as JavaScript's loose typing?
No. auto is fully static — the compiler deduces ONE fixed type at compile time and it never changes. auto x = 5; makes x an int forever; you cannot later store text in it. It saves typing, not type safety.
Q: When should I use optional instead of just returning -1 or nullptr?
Use std::optional whenever a function may legitimately have no answer (a lookup that misses, a parse that fails). It makes 'no value' a real, checked state instead of a magic number like -1 that a caller can forget to test. nullptr only works for pointers; optional works for any type.
Q: Why does my lambda crash after the function returns?
You almost certainly captured a local variable by reference ([&]) and then used the lambda after that variable went out of scope — a dangling reference. If the lambda outlives the variable, capture by value ([=] or [x]) so it keeps its own copy.
Q: Do I need a special compiler flag for these features?
Yes — pick the standard with a flag. C++17 features need -std=c++17 and C++20 features (concepts, ranges) need -std=c++20 on GCC/Clang. Our runner uses a modern standard, so the examples here compile as written.
Q: What's the difference between constexpr and const?
const means 'cannot be changed after it is set' (it may still be computed at run time). constexpr is stronger: the value must be computable at COMPILE time, so it can size arrays, be used in templates, and add zero run-time cost.
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.
🎯 Mini-Challenge: find the cheapest item
Return an optional cheapest item, then print it if one exists.
#include <iostream>
#include <vector>
#include <optional>
#include <string>
using namespace std;
// 🎯 MINI-CHALLENGE: Cheapest item finder
// 1. Make a vector of pair<string,double>: item name + price.
// e.g. {{"Pen", 1.50}, {"Mug", 6.00}, {"Pad", 2.25}}
// 2. Write a function returning optional<pair<string,double>>:
// the cheapest item, or nullopt if the list is empty.
// 3. Use a range-based for + structured bindings to compare.
// 4. In main(), if the optional has a value, pri
...🎉 Lesson Complete
- ✅
autodeduces a single static type — less typing, same safety - ✅ Range-based
forwithconst 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::optionalmodels "maybe a value" — check.has_value()/ use.value_or() - ✅
if/switchinitialisers andconstexprtighten scope and move work to compile time - ✅ Next lesson: Operator Overloading — give your own types natural
+,==, and<<behaviour
Sign up for free to track which lessons you've completed and get learning reminders.