Skip to main content
    Courses/C++/Modern C++ Features

    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

    💡 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

    StandardHeadline features
    C++11auto, range-based for, lambdas, nullptr, brace init
    C++14generic lambdas, relaxed constexpr
    C++17structured bindings, std::optional, if/switch with initialiser
    C++20concepts, 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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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::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)

    FeatureSyntaxSince
    Type deductionauto x = 42;C++11
    Range-based forfor (const auto& e : v)C++11
    Lambda[x](int n){ return n+x; }C++11
    Null pointerint* p = nullptr;C++11
    Brace initvector<int> v{1,2,3};C++11
    Structured bindingauto [a, b] = pair;C++17
    Optionaloptional<int> o = nullopt;C++17
    if / switch initif (auto r = f(); r) ...C++17
    constexpr fnconstexpr int sq(int x)C++11/14
    Concepts / rangestemplate<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.

    Try it Yourself »
    C++
    #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

    • 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

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service