Modern C++ Features: C++11 to C++20 Tutorial with Runnable Examples

Learn what changed from C++11 to C++20 by compiling one inventory program, tracing its exact output, and repairing common type, lifetime, and ownership mistakes.

KnowledgeGate Team

Exam prep & CS education

Updated 10 Aug 20266 min read

You may understand loops, classes, and raw pointers, yet code using auto, lambdas, smart pointers, structured bindings, and ranges can look like another language. Those five tools arrived across C++11, C++17 and C++20, and a single inventory program uses all of them together: it sorts three products by price, drops the item with zero stock, and prints a stock value of 9800. Compiling that program and predicting its output turns the version names into something you can actually see. For the wider order, from a first program through to the STL, follow C++ Tutorial: Complete Learning Path in 12 Weeks.

Modern C++ features: what the label actually covers

"Modern C++" means the safer, more expressive style introduced across successive C++ standards. It is not one library or one isolated feature.

  • C++11 introduced auto, nullptr, range-based for, lambdas, move semantics, and smart pointers.

  • C++14 added generic lambdas, so [](const auto& item) { return item.stock > 0; } accepts any type that has a stock member.

  • C++17 added structured bindings and std::optional.

  • C++20 added ranges and concepts.

These additions complement, rather than replace, the classic classes and inheritance covered in OOP for Teaching CS Exams: Classes and Inheritance. A class still models the data; auto, lambdas and smart pointers change how you name types, pass behaviour, and say which object owns a resource.

Save the inventory program as modern.cpp, then compile and run it with:

c++ -std=c++20 modern.cpp && ./a.out

An older compiler or a command without the C++20 option may reject the ranges syntax.

Start with type deduction, safer nulls, and range-based loops

Consider this warm-up:

std::vector<int> scores{42, 67, 91};
auto total = 0;
for (const auto score : scores) total += score;

The compiler infers total as an int. Each score is also an int, and const makes it read-only inside that iteration. The calculation is 42 + 67 + 91 = 200, so the total printed would be 200. auto asks the compiler to infer a static type. It does not make C++ dynamically typed.

References change the meaning. auto value = scores[0]; copies 42, while auto& value = scores[0]; makes value an alias for the first vector element. Changing the alias changes the element.

For pointers, use nullptr as the typed null-pointer value. In new code, avoid 0 or NULL when you mean a pointer. If loops, arrays, and pointer foundations need revision, first read C Programming for Teaching CS Exams: Key Concepts.

Build one runnable modern C++ inventory report

The program below sorts products by price, filters out items with no stock, and totals the remaining inventory value.

#include <algorithm>
#include <iostream>
#include <memory>
#include <ranges>
#include <string>
#include <utility>
#include <vector>

struct Product {
    std::string name;
    int price;
    int stock;
};

struct Report {
    int stock_value = 0;
};

int main() {
    std::vector<Product> products{
        {"Laptop Stand", 2200, 2},
        {"Mouse", 900, 0},
        {"Keyboard", 1800, 3}
    };

    std::ranges::sort(products, {}, &Product::price);
    auto in_stock = products | std::views::filter(
        [](const Product& product) { return product.stock > 0; }
    );
    auto report = std::make_unique<Report>();

    for (const auto& [name, price, stock] : in_stock) {
        std::cout << name << ": " << price << " x " << stock << '\n';
        report->stock_value += price * stock;
    }

    std::cout << "Stock value: " << report->stock_value << '\n';
}

Compiled and run with the command above, it prints:

Keyboard: 1800 x 3
Laptop Stand: 2200 x 2
Stock value: 9800

The program combines an owning std::vector, a projection-based ranges sort, a lambda predicate, a lazy filter view, structured bindings, and exclusive ownership through std::unique_ptr. Each tool solves a specific problem. None is a universal replacement for ordinary loops or stack objects.

Trace the lambda, range pipeline, and structured bindings

std::ranges::sort orders products by the projected Product::price. The order becomes Mouse at 900, Keyboard at 1800, then Laptop Stand at 2200. The lambda returns true only when stock > 0, so Mouse with stock 0 disappears. The view therefore yields Keyboard followed by Laptop Stand.

Now trace the total:

  1. Keyboard contributes 1800 x 3 = 5400.

  2. Laptop Stand contributes 2200 x 2 = 4400.

  3. The report stores 5400 + 4400 = 9800.

In const auto& [name, price, stock], the structured binding gives readable names to the three aggregate members. The reference avoids copying each Product, and const prevents mutation through those names.

The in_stock view is lazy. It describes how to traverse products; it does not build a second vector. Keep products alive, and do not structurally change it, while the view is in use.

Diagram of the C++20 inventory pipeline that sorts products by price, filters out the zero-stock item, and totals stock value to 9800.

Understand smart-pointer ownership and moving a value

auto report = std::make_unique<Report>(); creates one clear owner. When that owner goes out of scope, cleanup is automatic, so there is no manual delete. A stack variable would also be valid for this small Report; the smart pointer is here to teach ownership, not because the object must use heap storage.

Now extend the example:

auto archived = std::move(report);

Ownership moves to archived. Afterwards, report == nullptr is true, while archived->stock_value is still 9800. Writing auto archived = report; instead is a compile-time error because std::unique_ptr cannot be copied.

Use std::shared_ptr only when ownership is genuinely shared. It should not be the default escape from deciding which object owns a resource.

Diagram of std::move transferring the unique_ptr Report, leaving the source pointer null while the destination keeps stock_value 9800.

Common modern C++ mistakes and how to repair them

Confusing an auto copy with a reference

Start with Product keyboard{"Keyboard", 1800, 3};. After auto copy = keyboard; copy.price = 999;, keyboard.price remains 1800 because copy is separate. After auto& alias = keyboard; alias.price = 999;, the original price becomes 999. Name whether you intend to copy or alias instead of choosing syntax only because it is short.

Capturing a lambda by value or reference

Set int limit = 5;, create [limit] { return limit; } and [&limit] { return limit; }, then set limit = 9. The value-capturing lambda returns 5; the reference-capturing lambda returns 9. Never return a reference-capturing lambda if it can outlive the local variable it references.

Losing track of lifetime and ownership

Do not return a view over a local vector. Return an owning container instead. Do not dereference a moved-from smart pointer; test it or assign it a new resource first. Do not mix new or delete into this program; prefer automatic storage or a standard smart pointer.

How tests and interviews turn these features into questions

Common assessment tasks ask you to predict output, decide whether code copies or aliases, spot a compile-time ownership error, or trace a lambda and range pipeline. In an interview, explain both the result and the reason. Under exam timing the same habit decides what you attempt first, as Coding Round Strategy: Triage, Time-Box, Bank Marks sets out.

Try these before reading the answers:

  1. Change Mouse stock from 0 to 4. What is the order and total?

  2. Change the filter to stock >= 3. Which product remains?

  3. Move report into archived. What can each pointer access?

Answers

  1. Sorted output is Mouse, Keyboard, Laptop Stand. The total is 900 x 4 + 1800 x 3 + 2200 x 2 = 3600 + 5400 + 4400 = 13400.

  2. Only Keyboard remains, and its total is 1800 x 3 = 5400.

  3. report == nullptr is true, and archived->stock_value is 9800.

One final self-check: why use const auto& [name, price, stock] instead of auto [name, price, stock] when the loop only reads? The reference avoids copying the aggregate, and const prevents mutation through the bindings.

The short version and the next runnable step

Use auto to infer types without hiding intent. Prefer clear ownership, use lambdas for local behaviour, treat views as lazy non-owning traversal, and trace references and moves carefully.

For structured study with MCQs and coding questions, the Coding & DSA Courses for Placements hub lists the language courses, C++ included. Then compile the program above, apply each of the three exercise changes to the original code separately, and predict the output before you run it.