πŸ”’ Private Site

This site is password-protected.

Modern C++ (C++11 / 14 / 17 / 20)

C++ has transformed dramatically since C++11. Modern C++ looks and feels like a different language β€” safer, more expressive, and more powerful.


Table of Contents


Glossary β€” Key Terms at a Glance

Term Meaning
auto Compiler deduces the type β€” less verbose, fewer errors
Lambda Anonymous function object defined inline β€” capturable
constexpr Evaluate at compile time if possible
consteval Must evaluate at compile time (C++20)
optional Value that may or may not exist β€” replaces sentinel values
variant Type-safe union β€” holds one of several types
string_view Non-owning, zero-copy view of a string
Concept Named constraint on template parameters (C++20)
Range Composable lazy pipeline of operations (C++20)
CTAD Class Template Argument Deduction β€” compiler deduces template args
Coroutine Function that can suspend and resume execution

1 β€” C++11 β€” The Revolution

1.1 auto, Range-For, Uniform Init

Auto Type Deduction:

auto x = 42;                          // int
auto y = 3.14;                        // double
auto s = std::string("hello");        // std::string
auto it = myMap.begin();              // complex iterator type deduced

// Trailing return type (useful with templates)
auto add(int a, int b) -> int { return a + b; }

Range-Based For Loops:

std::vector<int> v{1, 2, 3, 4, 5};

for (auto x : v)          { /* copy */ }
for (auto& x : v)         { /* reference, modifiable */ }
for (const auto& x : v)   { /* const ref β€” PREFERRED for reading */ }

Uniform Initialization:

int x{42};
std::vector<int> v{1, 2, 3};
std::map<std::string, int> m{{"a", 1}, {"b", 2}};
Point p{3.0, 4.0};

// Prevents narrowing conversions
int x{3.14};  // ERROR β€” narrowing from double to int

1.2 nullptr & enum class

// nullptr β€” type-safe null pointer
int* p = nullptr;  // replaces NULL and 0

// Resolves ambiguity:
void foo(int);
void foo(int*);
foo(nullptr);  // calls foo(int*) β€” correct!
foo(NULL);     // might call foo(int) β€” ambiguous!
// enum class β€” scoped, strongly-typed enums
enum class Color : uint8_t { Red, Green, Blue };
Color c = Color::Red;
// No implicit conversion to int β€” safe!
// int x = c;  // ERROR

1.3 Lambda Expressions

Definition: A lambda is an anonymous function object defined inline. It can capture variables from the enclosing scope.

auto add = [](int a, int b) { return a + b; };
add(3, 4);  // 7

// Capture variables
int factor = 10;
auto scale = [factor](int x) { return x * factor; };   // capture by value
auto modify = [&factor](int x) { factor = x; };         // capture by reference

// Capture modes
auto f1 = [=]() { /* all by value */ };
auto f2 = [&]() { /* all by reference */ };
auto f3 = [=, &x]() { /* all by value, x by reference */ };

πŸ” Why Lambdas Matter: They replace verbose functor classes, enable clean algorithm usage (std::sort with custom comparator), and are the foundation of modern C++ functional style.


1.4 constexpr & static_assert

constexpr int factorial(int n) {
    return (n <= 1) ? 1 : n * factorial(n - 1);
}
constexpr int f10 = factorial(10);  // computed at COMPILE TIME

static_assert(sizeof(int) == 4, "int must be 4 bytes");
static_assert(std::is_integral_v<int>);  // C++17

1.5 Variadic Templates & tuple

template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args) << "\n";  // C++17 fold expression
}
print(1, " hello ", 3.14);  // "1 hello 3.14"

// std::tuple
auto t = std::make_tuple(1, "hello", 3.14);
auto [a, b, c] = t;  // C++17 structured bindings
std::get<0>(t);       // 1

2 β€” C++14 β€” Polish

2.1 Generic Lambdas & make_unique

// Generic lambdas β€” auto parameters
auto add = [](auto a, auto b) { return a + b; };
add(1, 2);       // int
add(1.5, 2.5);   // double

// Return type deduction
auto multiply(int a, int b) {
    return a * b;  // return type deduced as int
}

// make_unique (wasn't in C++11!)
auto p = std::make_unique<MyClass>(arg1, arg2);

2.2 Binary Literals & Digit Separators

int binary = 0b1010'1100;      // binary literal with separators
int million = 1'000'000;       // readable!
double pi = 3.141'592'653;

[[deprecated("Use newFunction() instead")]]
void oldFunction() { /* ... */ }

3 β€” C++17 β€” Major Feature Update

3.1 Structured Bindings

Definition: Structured bindings let you decompose aggregates (structs, pairs, tuples, arrays) into named variables in a single declaration.

std::map<std::string, int> m{{"alice", 90}, {"bob", 85}};

for (const auto& [name, score] : m) {
    std::cout << name << ": " << score << "\n";
}

auto [x, y] = std::make_pair(1, 2);

// Works with arrays too
int arr[] = {1, 2, 3};
auto [a, b, c] = arr;

3.2 If/Switch with Initializer

if (auto it = m.find("alice"); it != m.end()) {
    // use it->second
    // it is scoped to this if-else block
}

switch (auto val = compute(); val) {
    case 0: /* ... */ break;
    case 1: /* ... */ break;
}

Why this matters: The variable is scoped to the if/switch block β€” no leaking into the outer scope. Cleaner and safer code.


3.3 optional, variant, any

std::optional β€” Nullable value:

#include <optional>

std::optional<int> findUser(const std::string& name) {
    if (/* found */) return 42;
    return std::nullopt;
}

auto result = findUser("alice");
if (result) {
    std::cout << *result;
}
result.value_or(-1);  // default value if empty

std::variant β€” Type-safe union:

#include <variant>

std::variant<int, double, std::string> data;
data = 42;
data = 3.14;
data = "hello";

// Visit pattern
std::visit([](auto&& arg) {
    std::cout << arg << "\n";
}, data);

if (std::holds_alternative<int>(data)) {
    int val = std::get<int>(data);
}

std::any β€” Type-erased container:

#include <any>

std::any a = 42;
a = std::string("hello");
a = 3.14;
double d = std::any_cast<double>(a);  // 3.14

3.4 string_view

Definition: std::string_view is a non-owning, zero-copy reference to a character sequence. Just a pointer + size.

#include <string_view>

void process(std::string_view sv) {
    // No allocation, no copy β€” just a pointer + size
    sv.substr(0, 5);    // O(1) β€” returns another string_view
    sv.find("hello");
    sv.size();
}

process("hello world");               // no std::string allocation
std::string s = "hello";
process(s);                            // no copy
process(std::string_view(s).substr(0, 3));  // "hel" β€” no allocation

πŸ” Use string_view instead of const std::string& in function parameters when you don’t need ownership. It accepts both std::string and C-strings without allocation.

⚠️ Danger: string_view does NOT own the data. If the underlying string is destroyed, the string_view becomes a dangling reference. Never store a string_view that outlives its source.


3.5 filesystem

#include <filesystem>
namespace fs = std::filesystem;

fs::path p = "/home/user/data.csv";
p.filename();     // "data.csv"
p.extension();    // ".csv"
p.parent_path();  // "/home/user"

fs::exists(p);
fs::file_size(p);
fs::create_directories("/tmp/a/b/c");

for (const auto& entry : fs::directory_iterator("/home/user")) {
    std::cout << entry.path() << "\n";
}

3.6 Fold Expressions & CTAD

Fold Expressions (replace recursive variadic templates):

template<typename... Args>
auto sum(Args... args) {
    return (args + ...);        // right fold: a + (b + (c + d))
}

template<typename... Args>
void printAll(Args... args) {
    (std::cout << ... << args);  // left fold
}

sum(1, 2, 3, 4);  // 10

Class Template Argument Deduction (CTAD):

std::pair p{1, 2.0};           // deduced as pair<int, double>
std::vector v{1, 2, 3};        // deduced as vector<int>
std::optional o{42};            // deduced as optional<int>

3.7 constexpr if & Parallel Algorithms

constexpr if β€” compile-time branching:

template<typename T>
auto process(T val) {
    if constexpr (std::is_integral_v<T>) {
        return val * 2;
    } else if constexpr (std::is_floating_point_v<T>) {
        return val * 2.5;
    } else {
        return val;
    }
}
// Branches that don't match are NOT compiled β€” no SFINAE needed!

Parallel Algorithms:

#include <algorithm>
#include <execution>

std::vector<int> v(10'000'000);
std::sort(std::execution::par, v.begin(), v.end());  // parallel sort!
std::for_each(std::execution::par_unseq, v.begin(), v.end(),
    [](int& x) { x *= 2; });

[[nodiscard]] attribute (C++17): Forces callers to handle return values:

[[nodiscard]] int compute() { return 42; }
compute();  // WARNING: return value discarded

4 β€” C++20 β€” The Next Revolution

4.1 Concepts

Definition: Concepts are named constraints for template parameters. They replace SFINAE with readable, first-class syntax.

#include <concepts>

template<typename T>
concept Numeric = std::is_arithmetic_v<T>;

template<Numeric T>
T add(T a, T b) { return a + b; }

// Or with requires clause
template<typename T>
    requires std::integral<T>
T multiply(T a, T b) { return a * b; }

// Or abbreviated
auto divide(std::floating_point auto a, std::floating_point auto b) {
    return a / b;
}

πŸ” Why Concepts? SFINAE errors produce 100-line error messages. Concepts produce clear, one-line diagnostics: β€œT does not satisfy Numeric”.


4.2 Ranges

#include <ranges>
namespace rv = std::views;

std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// Composable, lazy operations
auto result = v
    | rv::filter([](int x) { return x % 2 == 0; })  // even numbers
    | rv::transform([](int x) { return x * x; });    // square them

for (int x : result) {
    std::cout << x << " ";  // 4 16 36 64 100
}

// No intermediate containers created!

πŸ” Why Ranges? They compose like Unix pipes. Lazy evaluation means no temporary vectors. Cleaner than chaining std::transform + std::copy_if manually.


4.3 std::format

#include <format>

std::string s = std::format("Hello, {}! You are {} years old.", name, age);
std::string f = std::format("{:.2f}", 3.14159);  // "3.14"
std::string h = std::format("{:#x}", 255);        // "0xff"
std::string w = std::format("{:>10}", "right");    // "     right"

4.4 consteval, constinit, Coroutines

consteval β€” guaranteed compile-time:

consteval int square(int x) { return x * x; }

int a = square(5);   // OK: evaluated at compile time
int b = 5;
// int c = square(b);  // ERROR: b is not constexpr

constinit β€” no static initialization order fiasco:

constinit int global = 42;  // guaranteed initialized at compile time

Coroutines:

#include <coroutine>

// Generator pattern
Generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        auto temp = a;
        a = b;
        b = temp + b;
    }
}

for (int x : fibonacci() | std::views::take(10)) {
    std::cout << x << " ";  // 0 1 1 2 3 5 8 13 21 34
}

4.5 Spaceship Operator & span

Three-Way Comparison (Spaceship Operator):

#include <compare>

struct Point {
    int x, y;
    auto operator<=>(const Point&) const = default;
    // Automatically generates: ==, !=, <, >, <=, >=
};

Point a{1, 2}, b{3, 4};
if (a < b) { /* ... */ }

std::span β€” non-owning view of contiguous data:

#include <span>

void process(std::span<int> data) {
    for (int x : data) { /* ... */ }
    data.size();
    data[0];
    data.subspan(2, 3);  // slice
}

int arr[] = {1, 2, 3, 4, 5};
std::vector<int> v{1, 2, 3};
process(arr);   // works
process(v);     // works

4.6 Modules

// math.cppm
export module math;

export int add(int a, int b) { return a + b; }
export int multiply(int a, int b) { return a * b; }

// main.cpp
import math;
import <iostream>;

int main() {
    std::cout << add(3, 4);  // 7
}

Modules replace #include β€” faster compilation, no header file issues, no macro pollution.


5 β€” Feature Timeline

5.1 Feature Timeline Summary

Feature C++11 C++14 C++17 C++20
auto βœ… Enhanced β€” β€”
Lambdas βœ… Generic β€” Template
Move semantics βœ… β€” β€” β€”
Smart pointers βœ… make_unique β€” β€”
constexpr Basic Relaxed if constexpr consteval
Structured bindings β€” β€” βœ… β€”
optional/variant β€” β€” βœ… β€”
string_view β€” β€” βœ… β€”
Concepts β€” β€” β€” βœ…
Ranges β€” β€” β€” βœ…
std::format β€” β€” β€” βœ…
Coroutines β€” β€” β€” βœ…
Modules β€” β€” β€” βœ…

Practice Questions

Q1. Explain auto type deduction. When does it deduce a reference? When does it decay? Give examples with const auto& and auto&&.

Q2. Write a lambda that captures a std::vector by reference, sorts it, and returns the median. What happens if you capture by value instead?

Q3. What is std::optional? How does it improve on returning sentinel values like -1 or nullptr? Write a function that uses it.

Q4. Compare std::string vs std::string_view. When is each appropriate? What dangers does string_view have?

Q5. Explain constexpr if with an example. How does it differ from a regular if? How does it replace SFINAE?

Q6. What are C++20 Concepts? Write a Sortable concept that constrains a template to types that support < comparison.

Q7. Write a Ranges pipeline that takes a vector of integers, filters out negatives, squares the remaining, and collects the top 5 results.

Q8. Explain the Spaceship Operator (<=>). What do strong_ordering, weak_ordering, and partial_ordering mean?

Q9. What problem do Modules solve? How do they improve compilation time compared to #include?

Q10. You have a C++03 codebase. List 10 modern C++ features you’d adopt first and why.


Key Takeaways

  1. Write C++17 at minimum β€” structured bindings, optional, string_view are essentials
  2. Use auto liberally β€” reduces verbosity, prevents implicit conversions
  3. Use string_view for function parameters β€” zero-cost string passing
  4. Use constexpr everything possible β€” catch errors at compile time
  5. Concepts replace SFINAE β€” much more readable template constraints
  6. Ranges compose beautifully β€” pipelines of lazy transformations
  7. std::format replaces printf and iostream formatting β€” type-safe, readable

← Back to C++ Notes