Chapter 5: Modern C++ Features (C++11 through C++23)

C++ has evolved dramatically since C++11. This chapter covers the most important features from each standard.

C++11: The Modern C++ Revolution

auto Type Inference

// Compiler deduces type from initializer
auto x = 42;              // int
auto y = 3.14;            // double
auto z = "hello";         // const char*
auto s = std::string("hello");  // std::string

// Essential for complex types
std::map<std::string, std::vector<int>> myMap;
auto it = myMap.begin();  // vs std::map<std::string, std::vector<int>>::iterator

// auto with references and const
int value = 42;
auto a = value;           // int (copy)
auto& b = value;          // int& (reference)
const auto& c = value;    // const int& (const reference)
auto* d = &value;         // int* (pointer)

Range-Based For Loops

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

// By value (copy)
for (int n : nums) {
    std::cout << n << " ";
}

// By reference (modify)
for (int& n : nums) {
    n *= 2;
}

// By const reference (read efficiently)
for (const int& n : nums) {
    std::cout << n << " ";
}

// With auto
for (const auto& n : nums) {
    std::cout << n << " ";
}

Lambda Expressions

// Basic lambda
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4) << "\n";  // 7

// Capture by value [=]
int multiplier = 10;
auto times = [=](int x) { return x * multiplier; };

// Capture by reference [&]
int counter = 0;
auto increment = [&]() { counter++; };
increment();
std::cout << counter << "\n";  // 1

// Mixed captures
int a = 1, b = 2, c = 3;
auto mixed = [a, &b, &c]() {  // a by value, b and c by reference
    // a++;  // ERROR: captured by value (immutable by default)
    b++;
    c++;
};

// Mutable lambda (can modify value captures)
auto mutableLambda = [a]() mutable {
    a++;  // OK with mutable
    return a;
};

// Generic lambda (C++14)
auto print = [](const auto& x) { std::cout << x << "\n"; };
print(42);
print("hello");
print(3.14);

// Lambda with explicit return type
auto divide = [](int a, int b) -> double {
    return static_cast<double>(a) / b;
};

Smart Pointers

#include <memory>

// unique_ptr - exclusive ownership
auto p1 = std::make_unique<int>(42);      // C++14
std::unique_ptr<int> p2(new int(42));     // C++11

// shared_ptr - shared ownership
auto p3 = std::make_shared<int>(42);
std::shared_ptr<int> p4 = p3;  // Reference count: 2

// weak_ptr - non-owning observer
std::weak_ptr<int> wp = p3;
if (auto locked = wp.lock()) {
    std::cout << *locked << "\n";
}

Move Semantics and Rvalue References

// Rvalue reference: binds to temporaries
void process(std::string&& str) {
    // str is now "owned" by this function
    data = std::move(str);  // Transfer ownership
}

std::string s = "hello";
process(std::move(s));  // s is now empty
process("temporary");   // Works with temporaries

// Perfect forwarding
template<typename T>
void wrapper(T&& arg) {
    actualFunction(std::forward<T>(arg));
}

nullptr

// Modern null pointer constant
int* p = nullptr;  // Clear and type-safe

// Old way (avoid)
int* q = NULL;     // Macro, can cause issues
int* r = 0;        // Works but unclear

Strongly-Typed Enums

// Old enum (unscoped)
enum Color { Red, Green, Blue };      // Pollutes namespace
int x = Red;                          // Implicit conversion to int

// New enum class (scoped)
enum class Color { Red, Green, Blue };
Color c = Color::Red;                 // Must use scope
// int x = Color::Red;                // ERROR: no implicit conversion
int x = static_cast<int>(Color::Red); // Explicit conversion required

// With underlying type
enum class Status : uint8_t {
    OK = 0,
    Error = 1,
    Pending = 2
};

constexpr

// Compile-time computation
constexpr int square(int x) {
    return x * x;
}

constexpr int result = square(5);  // Computed at compile time
int arr[square(3)];                // Array of size 9

// constexpr variables
constexpr double PI = 3.14159265358979;
constexpr int MAX_SIZE = 1024;

Initializer Lists

#include <initializer_list>

class IntVector {
    std::vector<int> data;
public:
    IntVector(std::initializer_list<int> list) : data(list) {}
};

IntVector v = {1, 2, 3, 4, 5};  // Uses initializer_list constructor

// Works with STL containers
std::vector<int> nums = {1, 2, 3};
std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}};

Variadic Templates

// Accept any number of arguments of any types
template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args) << "\n";  // Fold expression (C++17)
}

print(1, 2, 3);           // "123"
print("a", 1, 3.14);      // "a13.14"

// Recursive variadic template (C++11 way)
template<typename T>
void printOld(T t) {
    std::cout << t << "\n";
}

template<typename T, typename... Args>
void printOld(T t, Args... args) {
    std::cout << t << " ";
    printOld(args...);
}

C++14: Refinements

Generic Lambdas

auto add = [](auto a, auto b) { return a + b; };
add(1, 2);       // int
add(1.5, 2.5);   // double
add(std::string("a"), std::string("b"));  // string

Return Type Deduction

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

// Works with complex return types
auto makeVector() {
    return std::vector<int>{1, 2, 3};
}

Variable Templates

template<typename T>
constexpr T pi = T(3.14159265358979323846);

double d = pi<double>;
float f = pi<float>;

Binary Literals and Digit Separators

int binary = 0b1010'1010;        // Binary literal with separator
int million = 1'000'000;         // Easier to read
long long big = 1'000'000'000LL;

std::make_unique

// C++14 added make_unique (make_shared existed in C++11)
auto ptr = std::make_unique<MyClass>(arg1, arg2);

C++17: Major Additions

Structured Bindings

// Decompose pairs, tuples, structs, arrays
std::pair<int, std::string> p = {42, "hello"};
auto [num, str] = p;  // num = 42, str = "hello"

std::map<std::string, int> map = {{"a", 1}, {"b", 2}};
for (const auto& [key, value] : map) {
    std::cout << key << ": " << value << "\n";
}

struct Point { double x, y, z; };
Point pt = {1.0, 2.0, 3.0};
auto [x, y, z] = pt;

// Arrays
int arr[] = {1, 2, 3};
auto [a, b, c] = arr;

if/switch with Initializer

// Initialize variable in if statement
if (auto it = map.find("key"); it != map.end()) {
    std::cout << it->second << "\n";
}
// it is out of scope here

// Works with switch too
switch (auto val = compute(); val) {
    case 0: break;
    case 1: break;
    default: break;
}

std::optional

#include <optional>

std::optional<int> findValue(const std::string& key) {
    if (exists(key)) {
        return getValue(key);
    }
    return std::nullopt;  // No value
}

auto result = findValue("mykey");
if (result.has_value()) {
    std::cout << *result << "\n";
}

// Or use value_or for default
int val = result.value_or(0);

// Direct access (throws if empty)
try {
    int v = result.value();
} catch (const std::bad_optional_access&) {
    // Handle empty optional
}

std::variant

#include <variant>

// Type-safe union
std::variant<int, double, std::string> v;
v = 42;
v = 3.14;
v = "hello";

// Access with std::get
v = 42;
int i = std::get<int>(v);        // OK
// double d = std::get<double>(v);  // Throws std::bad_variant_access

// Safe access with std::get_if
if (auto* p = std::get_if<int>(&v)) {
    std::cout << *p << "\n";
}

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

// Overload pattern for different types
auto visitor = [](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>) {
        std::cout << "int: " << arg << "\n";
    } else if constexpr (std::is_same_v<T, std::string>) {
        std::cout << "string: " << arg << "\n";
    }
};

std::string_view

#include <string_view>

// Non-owning view of string data
void process(std::string_view sv) {
    // No allocation, just pointer + length
    std::cout << sv << "\n";
}

std::string s = "hello world";
process(s);              // From std::string
process("literal");      // From literal
process(s.substr(0, 5)); // From substring

// Create subviews without allocation
std::string_view view = "hello world";
std::string_view sub = view.substr(0, 5);  // "hello"

if constexpr

template<typename T>
auto getValue(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;  // Dereference if pointer
    } else {
        return t;   // Return directly otherwise
    }
}

int x = 42;
int* p = &x;
getValue(x);  // Returns 42
getValue(p);  // Returns 42 (dereferenced)

Fold Expressions

// Sum all arguments
template<typename... Args>
auto sum(Args... args) {
    return (... + args);  // ((arg1 + arg2) + arg3) + ...
}

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

// Print all arguments
template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args) << "\n";
}

// Check if all are true
template<typename... Args>
bool allTrue(Args... args) {
    return (... && args);
}

std::filesystem

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

// Path operations
fs::path p = "/home/user/documents/file.txt";
std::cout << p.filename() << "\n";     // "file.txt"
std::cout << p.extension() << "\n";    // ".txt"
std::cout << p.parent_path() << "\n";  // "/home/user/documents"

// Check existence
if (fs::exists(p)) {
    std::cout << "File exists\n";
}

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

// Recursive iteration
for (const auto& entry : fs::recursive_directory_iterator("/path")) {
    if (entry.is_regular_file()) {
        std::cout << entry.path() << ": " << entry.file_size() << " bytes\n";
    }
}

// Create directories
fs::create_directories("/path/to/new/dir");

// Copy, rename, remove
fs::copy("source.txt", "dest.txt");
fs::rename("old.txt", "new.txt");
fs::remove("file.txt");
fs::remove_all("directory");  // Recursive

Parallel Algorithms

#include <algorithm>
#include <execution>
#include <vector>

std::vector<int> data(1000000);

// Sequential (default)
std::sort(data.begin(), data.end());

// Parallel
std::sort(std::execution::par, data.begin(), data.end());

// Parallel and vectorized
std::sort(std::execution::par_unseq, data.begin(), data.end());

// Works with many algorithms
std::for_each(std::execution::par, data.begin(), data.end(),
    [](int& x) { x *= 2; });

auto result = std::reduce(std::execution::par, data.begin(), data.end());

C++20: Major Language Update

Concepts

#include <concepts>

// Define requirements for types
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template<typename T>
concept Printable = requires(T t) {
    { std::cout << t } -> std::same_as<std::ostream&>;
};

// Use concepts to constrain templates
template<Numeric T>
T add(T a, T b) {
    return a + b;
}

// Shorthand syntax
void print(Printable auto const& value) {
    std::cout << value << "\n";
}

// requires clause
template<typename T>
requires std::integral<T>
T factorial(T n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

Ranges

#include <ranges>
#include <vector>

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

// Views are lazy and composable
auto result = nums
    | std::views::filter([](int n) { return n % 2 == 0; })  // Even numbers
    | std::views::transform([](int n) { return n * n; });   // Square them

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

// Common views
auto first5 = nums | std::views::take(5);
auto skip3 = nums | std::views::drop(3);
auto reversed = nums | std::views::reverse;
auto indexed = std::views::iota(0, 10);  // 0, 1, 2, ..., 9

// Range algorithms
std::ranges::sort(nums);
auto it = std::ranges::find(nums, 5);
bool hasEven = std::ranges::any_of(nums, [](int n) { return n % 2 == 0; });

Coroutines

#include <coroutine>
#include <generator>  // C++23 or use library

// Generator coroutine
std::generator<int> range(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;
    }
}

// Usage
for (int i : range(0, 10)) {
    std::cout << i << " ";
}

// Async coroutine (simplified)
Task<int> asyncCompute() {
    auto result = co_await asyncOperation();
    co_return result * 2;
}

Modules

// math.ixx (module interface)
export module math;

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

export class Calculator {
public:
    int multiply(int a, int b) { return a * b; }
};

// main.cpp
import math;

int main() {
    int sum = add(3, 4);
    Calculator calc;
    int product = calc.multiply(3, 4);
}

Three-Way Comparison (Spaceship Operator)

#include <compare>

struct Version {
    int major, minor, patch;

    auto operator<=>(const Version&) const = default;
};

Version v1{1, 2, 3};
Version v2{1, 3, 0};

if (v1 < v2) { }   // true
if (v1 == v2) { }  // false
if (v1 != v2) { }  // true
// All comparison operators work!

consteval and constinit

// consteval: MUST be evaluated at compile time
consteval int compileTimeSquare(int x) {
    return x * x;
}

constexpr int a = compileTimeSquare(5);  // OK
// int b = compileTimeSquare(runtime_value);  // ERROR

// constinit: initialized at compile time, but not const
constinit int global = 42;  // Prevents static init order fiasco
// global = 100;  // OK: can be modified at runtime

std::format

#include <format>

std::string s = std::format("Hello, {}!", "World");
std::string nums = std::format("{} + {} = {}", 1, 2, 3);

// Formatting options
std::format("{:>10}", 42);       // "        42" (right align)
std::format("{:<10}", 42);       // "42        " (left align)
std::format("{:^10}", 42);       // "    42    " (center)
std::format("{:06}", 42);        // "000042" (zero-padded)
std::format("{:.2f}", 3.14159);  // "3.14" (precision)
std::format("{:x}", 255);        // "ff" (hex)
std::format("{:b}", 42);         // "101010" (binary)

// Print directly
std::print("Hello, {}!\n", "World");  // C++23

C++23: Latest Additions

std::expected

#include <expected>

std::expected<int, std::string> divide(int a, int b) {
    if (b == 0) {
        return std::unexpected("Division by zero");
    }
    return a / b;
}

auto result = divide(10, 2);
if (result.has_value()) {
    std::cout << *result << "\n";  // 5
} else {
    std::cout << "Error: " << result.error() << "\n";
}

// Monadic operations
auto doubled = divide(10, 2)
    .transform([](int x) { return x * 2; })
    .value_or(0);

std::print

#include <print>

std::print("Hello, {}!\n", "World");
std::println("Value: {}", 42);  // Adds newline automatically

// To other streams
std::print(std::cerr, "Error: {}\n", message);

Deducing this

class Builder {
    std::string data;

public:
    // Single implementation for both lvalue and rvalue
    template<typename Self>
    auto&& add(this Self&& self, std::string_view s) {
        self.data += s;
        return std::forward<Self>(self);
    }
};

Builder b;
b.add("hello").add(" ").add("world");  // Chaining works
Builder().add("temp").add("chain");    // Move semantics preserved

std::mdspan

#include <mdspan>

// Multidimensional view over contiguous data
std::vector<int> data(12);
std::mdspan<int, std::extents<size_t, 3, 4>> matrix(data.data());

matrix[1, 2] = 42;  // 2D indexing

Feature Detection Macros

// Check for feature availability
#if __cpp_concepts >= 201907L
    // Use concepts
#endif

#if __cpp_ranges >= 201911L
    // Use ranges
#endif

// Version check
#if __cplusplus >= 202002L
    // C++20 or later
#elif __cplusplus >= 201703L
    // C++17
#elif __cplusplus >= 201402L
    // C++14
#elif __cplusplus >= 201103L
    // C++11
#endif

Exercises

  1. Lambda Practice

    • Write a function that returns a counter lambda (captures and increments)
    • Write a generic lambda that prints any container
  2. std::optional Usage

    • Create a function that searches a vector and returns optional
    • Chain optional operations with value_or
  3. Ranges Pipeline

    • Create a pipeline that filters, transforms, and takes elements
    • Compare readability with traditional loop approach
  4. Concepts

    • Define a concept for "Container" (has begin(), end(), size())
    • Write a generic print function constrained by the concept

Previous: 04 - Object-Oriented Programming Next: 06 - STL Deep Dive