Modern C++

For over a decade the term “modern C++” has been widely used by the C++ community to mean C++11 (the 2011 language standard) or later. Since that revision of the standard the language has been undergoing a quiet (and not so quiet) revolution, which has brought it inline with recent trends in programming languages, increased its versatility and usability, and enhanced the standard library.

Questions on modern C++ are particularly common in interview questions on the language. Seasoned C++ developers may be testing whether novices and/or other seasoned C++ developers have been staying up-to-date with changes in the language. Thus if you are looking for a C++-related job it’s a good idea to know about these language changes.

We’ll summarize some of them in this article, which features more than 70 code examples.

You can find all code examples listed here in the Git repository http://github.com/thalesians/modern_cpp/

C++11

Language features

Automatic type deduction with auto

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{1,2,3};
    auto x = v[1];
    std::cout << x << "\n";
}

This example demonstrates automatic type deduction with auto, introduced in C++11, which allows the compiler to infer the type of a variable from its initializer. Here, auto x = v[1]; tells the compiler to deduce the type of x from the expression v[1], which is an int, eliminating the need to explicitly spell out the type and reducing verbosity without sacrificing type safety. The deduced type is fixed at compile time (unlike dynamic typing), making auto especially useful when working with complex iterator or template-heavy types, while still producing clear, efficient, and statically checked code.

Dedicated null pointer literal

#include <iostream>

void f(int)   { std::cout << "int\n"; }
void f(char*) { std::cout << "ptr\n"; }

int main() {
    f(nullptr); // calls pointer overload
}

This example demonstrates the introduction of nullptr in C++11 as a dedicated null pointer literal, designed to replace the ambiguous use of integer literals such as 0 or NULL when representing null pointers. In this program, the overloaded functions f(int) and f(char*) illustrate a classic problem in pre-C++11 code: passing 0 or NULL could incorrectly select the integer overload. By using nullptr, which has its own type (std::nullptr_t) and is implicitly convertible only to pointer types (and pointer-to-member types), the call f(nullptr) unambiguously resolves to the pointer overload, improving both type safety and overload resolution clarity.

Strongly typed enumerations

#include <iostream>

enum class Colour { Red, Green };

int main() {
    Colour c = Colour::Red;
    std::cout << (c == Colour::Red) << "\n";
}

This example demonstrates strongly typed enumerations (enum class), introduced in C++11, which address several long-standing issues with traditional unscoped enums. By using enum class Colour, the enumerators (Red, Green) are scoped to the enumeration type and do not implicitly convert to integers, preventing accidental misuse and name collisions. In the example, values must be explicitly qualified (Colour::Red), and comparisons such as c == Colour::Red are type-safe and unambiguous, resulting in clearer, more robust code—particularly in large codebases or APIs where enum values should not silently interact with unrelated integral types.

Range-based for loop

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{1,2,3};
    int sum = 0;
    for (int a : v) sum += a;
    std::cout << sum << "\n";
}

This example demonstrates the range-based for loop, introduced in C++11, which provides a concise and expressive way to iterate over all elements of a range such as a container or array. Instead of explicitly managing iterators or indices, the loop for (int a : v) automatically traverses the elements of the std::vector, binding each element in turn to the loop variable a. This improves readability, reduces boilerplate, and minimizes common errors such as off-by-one mistakes, while still compiling to efficient code equivalent to a traditional iterator-based loop.

Compile-time assertions

#include <type_traits>

int main() {
    static_assert(sizeof(void*) >= 4, "weird platform");
    static_assert(std::is_integral_v<int>, "int should be integral");
}

This example demonstrates compile-time assertions with static_assert, introduced in C++11, which allow programmers to enforce invariants and assumptions during compilation rather than at runtime. The first static_assert checks a platform property (sizeof(void*) >= 4), while the second uses a type trait (std::is_integral_v<int>) to assert a property of a type; if either condition is false, compilation fails with the provided diagnostic message. This mechanism is particularly valuable for writing portable, generic, and template-heavy code, as it surfaces configuration or type errors early and unambiguously, long before the program is run.

constexpr functions

#include <iostream>

constexpr int sq(int x) { return x * x; }

int main() {
    constexpr int a = sq(12);
    std::cout << a << "\n";
}

This example demonstrates constexpr functions, introduced in C++11, which allow certain computations to be evaluated at compile time when given constant-expression arguments. The function sq is declared constexpr, meaning the compiler is permitted (and in this case required) to evaluate sq(12) during compilation, producing the constant a without any runtime overhead. At the same time, constexpr functions remain ordinary functions that can also be called at runtime with non-constant arguments, making them a powerful tool for expressing intent, enabling compile-time optimization, and writing safer, more expressive code that bridges compile-time and runtime computation.

Delegating constructors

#include <iostream>

struct X {
    int a, b;
    X(int a_, int b_) : a(a_), b(b_) {}
    X(int a_) : X(a_, 0) {} // delegates
};

int main() {
    X x(5);
    std::cout << x.a << " " << x.b << "\n";
}

This example demonstrates delegating constructors, introduced in C++11, which allow one constructor of a class to forward its initialization work to another constructor in the same class. In the struct X, the single-argument constructor X(int a_) delegates to the two-argument constructor X(int a_, int b_), ensuring that all initialization logic is centralized and consistent. This avoids code duplication, reduces the risk of divergent initialization paths, and makes constructor behavior easier to reason about, particularly as classes evolve and gain additional constructors.

In-class member initializers

#include <iostream>

struct S {
    int x = 10;
    int y = 20;
};

int main() {
    S s;
    std::cout << s.x << " " << s.y << "\n";
}

This example demonstrates in-class member initializers, introduced in C++11, which allow data members to be given default values directly at their point of declaration. In the struct S, the members x and y are initialized to 10 and 20 without requiring an explicit constructor, ensuring that every instance of S starts in a well-defined state. In-class initializers simplify class definitions, reduce boilerplate constructor code, and make default object semantics clearer and less error-prone, especially when multiple constructors or aggregate usage are involved.

Lambda expressions

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{3,1,2};
    std::sort(v.begin(), v.end(), [](int a, int b){ return a < b; });
    std::cout << v[0] << "\n";
}

This example demonstrates lambda expressions, introduced in C++11, which provide a concise way to define anonymous function objects directly at the point of use. The lambda [](int a, int b){ return a < b; } is passed to std::sort as a custom comparison function, eliminating the need to define a separate named functor or free function. Lambdas improve locality and readability by keeping simple behavior close to where it is used, enable inline customization of algorithms, and compile to efficient code comparable to hand-written function objects.

The override specifier

#include <iostream>

struct B { virtual int f() const { return 1; } };
struct D : B { int f() const override { return 2; } };

int main() {
    B* p = new D();
    std::cout << p->f() << "\n";
    delete p;
}

This example demonstrates the override specifier, introduced in C++11, which is used to explicitly indicate that a virtual member function is intended to override a virtual function from a base class. In the derived struct D, marking f() with override instructs the compiler to verify that the function’s signature exactly matches a virtual function in B; if it does not, compilation fails. This provides an important safety check against subtle bugs caused by mismatched const qualifiers, parameter types, or return types, while still enabling standard runtime polymorphism as shown by calling f() through a base-class pointer.

Move semantics

#include <iostream>
#include <string>
#include <utility>

int main() {
    std::string a = "hello";
    std::string b = std::move(a); // moves from a into b
    std::cout << "b=" << b << "\n";
    std::cout << "a.size()=" << a.size() << "\n"; // valid but unspecified contents
}

This example demonstrates move semantics, introduced in C++11, specifically the use of std::move to enable efficient transfer of resources from one object to another. By applying std::move(a), the program converts a into an rvalue reference, allowing the std::string move constructor to transfer ownership of its internal buffer to b rather than performing a costly deep copy. After the move, a remains in a valid but unspecified state—as shown by safely querying a.size()—highlighting how move semantics improve performance while preserving well-defined object lifetimes and safety guarantees.

Standard library enhancements

Chrono time utilities

#include <chrono>
#include <iostream>

int main() {
    auto t0 = std::chrono::steady_clock::now();
    volatile long long s = 0;
    for (long long i = 0; i < 10'000'00; ++i) s += i;
    auto t1 = std::chrono::steady_clock::now();

    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count();
    std::cout << ms << " ms\n";
}

This example demonstrates the <chrono> time utilities, introduced in C++11, which provide a type-safe, high-resolution framework for measuring and manipulating time. The program records two timestamps using std::chrono::steady_clock, a monotonic clock suitable for interval measurement, computes the elapsed duration between them, and converts that duration to milliseconds with std::chrono::duration_cast. By expressing time in strongly typed units rather than raw integers, <chrono> prevents unit-related errors, improves code clarity, and enables precise, portable performance measurement.

In-place construction with emplace_back

#include <iostream>
#include <string>
#include <vector>

struct S {
    int id; std::string name;
    S(int i, std::string n) : id(i), name(std::move(n)) {}
};

int main() {
    std::vector<S> v;
    v.emplace_back(7, "Ada");
    std::cout << v[0].name << "\n";
}

This example demonstrates in-place construction with emplace_back, introduced in C++11, which allows elements to be constructed directly within a container rather than being created as temporary objects and then copied or moved into place. The call v.emplace_back(7, "Ada") forwards its arguments to the constructor of S, constructing the object directly inside the std::vector’s storage. This approach improves efficiency, avoids unnecessary temporaries, and makes container insertion both more expressive and better aligned with modern C++’s emphasis on move semantics and perfect forwarding.

Hash tables

#include <iostream>
#include <string>
#include <unordered_map>

int main() {
    std::unordered_map<std::string, int> m;
    m["CL"] = 1;
    m["NG"] = 2;
    std::cout << m["NG"] << "\n";
}

This example demonstrates the use of std::unordered_map, introduced in C++11, which provides an associative container implemented as a hash table rather than a tree. Unlike ordered containers such as std::map, std::unordered_map offers average constant-time complexity for insertions, lookups, and updates by hashing keys instead of maintaining sorted order. In the example, string keys are mapped to integer values and accessed via the subscript operator, illustrating a common and efficient pattern for building fast key–value lookup tables when ordering of elements is not required.

Line-oriented text input

#include <iostream>
#include <sstream>
#include <string>

int main() {
    std::istringstream in("a\nb\n");
    std::string line;
    while (std::getline(in, line)) {
        std::cout << "[" << line << "]\n";
    }
}

This example demonstrates line-oriented text input using std::getline, a core I/O facility of the C++ standard library that works seamlessly with stream types such as std::istringstream. The loop reads input one line at a time, stopping at newline boundaries and storing each line in a std::string, which is then processed or printed. This pattern is fundamental for parsing text-based formats, as it cleanly separates line handling from token extraction, avoids buffer overflows, and works uniformly across files, string streams, and standard input.

Binary I/O using stream classes

#include <fstream>
#include <iostream>

int main() {
    { // write
        std::ofstream out("x.bin", std::ios::binary);
        int v = 123;
        out.write(reinterpret_cast<const char*>(&v), sizeof(v));
    }
    { // read
        std::ifstream in("x.bin", std::ios::binary);
        int v = 0;
        in.read(reinterpret_cast<char*>(&v), sizeof(v));
        std::cout << v << "\n";
    }
}

This example demonstrates binary file I/O using stream classes, a long-standing but essential capability of the C++ standard library that is particularly important for performance-sensitive or low-level data handling. By opening std::ofstream and std::ifstream with the std::ios::binary flag, the program writes and reads the raw byte representation of an int directly to and from a file using the write and read member functions. This approach bypasses text formatting, preserves exact binary layouts, and enables efficient serialization of fixed-size data, while still benefiting from RAII-based resource management and type-safe stream abstractions.

Basic multithreading

#include <iostream>
#include <thread>

int main() {
    int x = 0;
    std::thread t([&]{ x = 42; });
    t.join();
    std::cout << x << "\n";
}

This example demonstrates basic multithreading with std::thread, introduced in C++11, which provides a portable, standard way to create and manage operating-system threads in C++. The program launches a new thread executing a lambda that modifies a shared variable, then synchronizes with that thread by calling join(), ensuring that the thread completes before the program proceeds. This illustrates the fundamental thread lifecycle—creation, execution, and joining—while highlighting the need for explicit synchronization to coordinate work across threads in concurrent programs.

Regular expressions

#include <iostream>
#include <regex>
#include <string>

int main() {
    std::string s = "PX=12.34 USD";
    std::regex r(R"(PX=([0-9]+\.[0-9]+))");
    std::smatch m;
    if (std::regex_search(s, m, r)) {
        std::cout << m[1] << "\n";
    }
}

This example demonstrates regular expression support via <regex>, introduced in C++11, which brings standardized pattern matching and text searching facilities into the C++ standard library. The program defines a regular expression to capture a floating-point value following the prefix PX=, applies std::regex_search to find the pattern within a larger string, and extracts the matched subexpression using std::smatch. This feature enables expressive and declarative text processing directly in C++ without relying on external libraries, making it suitable for tasks such as validation, parsing, and log or message analysis.

C++14

Language features

Generic lambdas

#include <iostream>

int main() {
    auto add = [](auto a, auto b) { return a + b; };
    std::cout << add(2, 3) << "\n";
    std::cout << add(1.5, 2.0) << "\n";
}

This example demonstrates generic lambdas, introduced in C++14, which allow lambda parameters to be declared with auto, making the lambda function itself a template. The lambda [](auto a, auto b) { return a + b; } can be invoked with different argument types, as shown by adding both integers and floating-point values, with the compiler generating the appropriate instantiations at compile time. Generic lambdas greatly increase the flexibility and reuse of small function objects while retaining static type checking and zero runtime overhead.

decltype(auto), preserving reference-ness

#include <iostream>

int& ref(int& x) { return x; }

int main() {
    int a = 7;
    decltype(auto) r = ref(a); // r is int&
    r = 99;
    std::cout << a << "\n";
}

This example demonstrates decltype(auto), introduced in C++14, which instructs the compiler to deduce a variable’s type using the rules of decltype rather than the usual auto deduction. In this case, ref(a) returns an int&, and by declaring decltype(auto) r = ref(a);, the reference nature of the return type is preserved, making r an alias to a. This feature is particularly useful when writing forwarding code or generic functions where it is important to retain exact value categories and reference qualifiers instead of decaying them away.

Standard library enhancements

Chrono user-defined literals

#include <chrono>
#include <iostream>
using namespace std::chrono_literals;

int main() {
    auto d = 150ms + 2s;
    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(d).count() << "\n";
}

This example demonstrates user-defined literals for <chrono> durations, introduced in C++14, which provide a concise and readable way to express time intervals directly in source code. By bringing std::chrono_literals into scope, suffixes such as ms and s can be used to construct strongly typed duration objects (150ms, 2s) without verbose type names. These duration literals can be combined with normal arithmetic and safely converted between units using duration_cast, improving clarity, correctness, and expressiveness when working with time-based logic.

std::quoted I/O manipulator

#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>

int main() {
    std::ostringstream out;
    out << std::quoted("hello world");
    std::cout << out.str() << "\n"; // "hello world"

    std::istringstream in(out.str());
    std::string s;
    in >> std::quoted(s);
    std::cout << s << "\n";
}

This example demonstrates the std::quoted I/O manipulator, introduced in C++14, which simplifies reading and writing quoted strings with proper escaping using standard streams. When outputting, std::quoted("hello world") automatically adds quotation marks and escapes characters as needed; when inputting, the same manipulator correctly removes the quotes and decodes any escape sequences. This feature is particularly useful for serializing and deserializing text fields that may contain spaces or special characters, allowing robust, reversible string I/O without custom parsing logic.

std::exchange

#include <iostream>
#include <utility>

int main() {
    int x = 10;
    int old = std::exchange(x, 42);
    std::cout << old << " " << x << "\n";
}

This example demonstrates std::exchange, introduced in C++14, a small but expressive utility that replaces the value of an object while simultaneously returning its previous value. In the program, std::exchange(x, 42) assigns 42 to x and yields the original value 10, which is stored in old. This idiom is particularly useful when implementing move operations, state transitions, or resource handoff patterns, as it concisely captures the common “set new value and retrieve old value” operation in a single, clear expression.

std::unique_ptr

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> p = std::make_unique<int>(42);
    std::cout << *p << "\n";
}

This example demonstrates std::make_unique, introduced in C++14, which provides a safe and concise way to create objects managed by std::unique_ptr. By calling std::make_unique<int>(42), the program allocates an int and transfers ownership directly into a std::unique_ptr, eliminating the possibility of memory leaks that can arise from manually pairing new with smart pointers. This factory function improves exception safety, reduces verbosity, and clearly expresses exclusive ownership semantics in modern C++ code.

C++17

Language features

Class template argument deduction (CTAD)

#include <iostream>
#include <utility>

int main() {
    std::pair p(1, 2.5); // deduces std::pair<int, double>
    std::cout << p.first << " " << p.second << "\n";
}

This example demonstrates class template argument deduction (CTAD), introduced in C++17, which allows the compiler to deduce the template arguments of a class from its constructor arguments. In the statement std::pair p(1, 2.5);, the compiler infers the type as std::pair<int, double> without requiring explicit template parameters, reducing verbosity while preserving full static type safety. CTAD makes generic code more readable and approachable, particularly when constructing standard library types whose template arguments are obvious from context.

if constexpr

#include <iostream>
#include <type_traits>

template<class T>
void f(T x) {
    if constexpr (std::is_integral_v<T>) std::cout << "int-like\n";
    else                                 std::cout << "non-int\n";
    (void)x;
}

int main() {
    f(123);
    f(1.2);
}

This example demonstrates if constexpr, introduced in C++17, which enables compile-time conditional branching within templates. Unlike a regular if statement, if constexpr discards the non-selected branch during compilation based on the template parameter—in this case, whether T is an integral type as determined by std::is_integral_v<T>. This allows a single function template to express type-specific behavior without requiring specialization or risking compilation errors from ill-formed code in unused branches, resulting in clearer, safer, and more efficient generic code.

“Initializer statement” form of if

#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string,int> m{{"a",1},{"b",2}};
    if (auto it = m.find("b"); it != m.end()) {
        std::cout << it->second << "\n";
    }
}

This example demonstrates the C++17 “initializer statement” form of if, which allows a variable to be declared and initialized directly within the if statement itself. In the expression if (auto it = m.find("b"); it != m.end()), the iterator it is scoped to the if statement, preventing accidental use outside the condition and keeping related logic tightly localized. This feature improves code clarity and safety by limiting variable lifetimes, reducing namespace pollution, and making common lookup-and-check patterns more concise and expressive.

Inline variables

#include <iostream>

inline int g = 7; // single definition across TUs (if header-included)

int main() {
    std::cout << g << "\n";
}

This example demonstrates inline variables, introduced in C++17, which allow variables with external linkage to be defined in header files without violating the One Definition Rule. By declaring inline int g = 7;, the variable can be included in multiple translation units while still referring to a single shared instance, analogous to how inline functions work. This feature simplifies the definition of global constants and header-only libraries, eliminating the need for separate declarations and definitions while preserving well-defined linkage and initialization semantics.

[[maybe_unused]]

#include <iostream>

int main() {
    [[maybe_unused]] int debug_only = 123;
    std::cout << "ok\n";
}

This example demonstrates the [[maybe_unused]] attribute, introduced in C++17, which allows programmers to explicitly mark variables, parameters, or other entities that may intentionally go unused. By annotating debug_only with [[maybe_unused]], the code suppresses compiler warnings about unused variables while still documenting intent for readers and tools. This attribute is particularly useful in conditional compilation, debugging code, or generic implementations where certain entities are only used in some configurations, helping keep builds warning-clean without resorting to less expressive workarounds.

Structured bindings

#include <iostream>
#include <tuple>

int main() {
    std::tuple<int, double> t{7, 3.14};
    auto [i, d] = t;
    std::cout << i << " " << d << "\n";
}

This example demonstrates structured bindings, introduced in C++17, which allow a composite object to be decomposed into named variables in a single, readable declaration. The statement auto [i, d] = t; unpacks the elements of the std::tuple into separate variables with automatically deduced types, eliminating the need for verbose std::get<>() calls. Structured bindings improve clarity when working with tuples, pairs, and other decomposable types, and they encourage more expressive, self-documenting code when handling grouped return values or data structures.

Variadic fold expressions

#include <iostream>

template <class... Ts>
auto sum(Ts... xs) {
    return (xs + ...); // fold
}

int main() {
    std::cout << sum(1, 2, 3, 4) << "\n";
}

This example demonstrates fold expressions, introduced in C++17, which provide a concise way to apply a binary operator to all elements of a parameter pack. The expression (xs + ...) folds the + operator over the variadic template arguments, expanding to a chain of additions without requiring recursive template instantiations or helper functions. Fold expressions significantly simplify variadic template code, improving readability, compile-time performance, and maintainability when aggregating or combining an arbitrary number of arguments.

Standard library enhancements

std::any

#include <any>
#include <iostream>
#include <string>

int main() {
    std::any a = std::string("hello");
    std::cout << std::any_cast<std::string>(a) << "\n";
}

This example demonstrates std::any, introduced in C++17, which provides a type-safe container for holding a single value of any copyable type with runtime type information. In the example, a std::string is stored inside a std::any object and later retrieved using std::any_cast, which checks that the requested type matches the stored value. std::any is useful when the set of possible types is not known at compile time, offering safer semantics than raw void* while still enabling flexible, type-erased designs.

try_emplace for associative containers

#include <iostream>
#include <string>
#include <unordered_map>

int main() {
    std::unordered_map<std::string, int> m;
    m.try_emplace("A", 1);  // inserts
    m.try_emplace("A", 999); // does nothing (key exists)
    std::cout << m["A"] << "\n";
}

This example demonstrates try_emplace for associative containers, introduced in C++17, which provides an efficient way to insert elements into maps without unnecessary construction or assignment. The call m.try_emplace("A", 1) inserts a new element only if the key does not already exist, while the second call with the same key has no effect and does not overwrite the existing value. Unlike operator[] or insert, try_emplace avoids constructing the mapped value when insertion fails, making it particularly useful for performance-sensitive code and for expressing clear “insert-if-absent” semantics.

std::clamp

#include <algorithm>
#include <iostream>

int main() {
    int x = 15;
    std::cout << std::clamp(x, 0, 10) << "\n";
}

This example demonstrates std::clamp, introduced in C++17, a small utility function that constrains a value to lie within a specified closed interval. The call std::clamp(x, 0, 10) returns x if it is between the bounds, the lower bound if x is too small, or the upper bound if x is too large—in this case producing 10. std::clamp improves readability and correctness by replacing ad-hoc min/max logic with a clear, self-documenting expression that is easy to reason about and free from common boundary errors.

Low-level, locale-independent numeric formatting

#include <charconv>
#include <iostream>

int main() {
    char buf[32];
    int x = 12345;
    auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), x);
    if (ec == std::errc{}) std::cout.write(buf, ptr - buf), std::cout << "\n";
}

This example demonstrates low-level, locale-independent numeric formatting with std::to_chars, introduced in C++17, which converts numbers directly into character buffers without allocating memory or using streams. The function writes the textual representation of x into a user-provided buffer and reports success or failure via an error code, returning a pointer to the end of the written sequence. This facility is designed for high-performance scenarios—such as logging, serialization, or parsing pipelines—where predictability, speed, and minimal overhead are more important than the convenience of formatted I/O streams.

Low-level, allocation-free numeric parsing

#include <charconv>
#include <iostream>
#include <string_view>

int main() {
    std::string_view s = "12345";
    int x = 0;
    auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), x);
    if (ec == std::errc{}) std::cout << x << "\n";
}

This example demonstrates low-level, allocation-free numeric parsing with std::from_chars, introduced in C++17, which converts a sequence of characters into a numeric value without using streams or locale-dependent formatting. The function attempts to parse the integer represented by the character range and reports the result via an error code, allowing the caller to detect success or failure without exceptions. std::from_chars is particularly valuable in performance-critical code paths—such as parsers, data ingestion systems, or protocol handlers—where predictable behavior, minimal overhead, and precise control over error handling are essential.

The <filesystem> library

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    fs::path p = fs::temp_directory_path() / "cpp_fs_demo";
    fs::create_directories(p);
    std::cout << p.string() << "\n";
}

This example demonstrates the <filesystem> library, introduced in C++17, which provides a portable, type-safe interface for interacting with the file system. The code constructs a std::filesystem::path, combines it with the system’s temporary directory using the / path-concatenation operator, and creates the directory hierarchy with create_directories. By replacing platform-specific APIs and string-based path manipulation with a unified abstraction, <filesystem> simplifies common tasks such as path handling, directory creation, and file inspection while improving correctness and cross-platform portability.

Map-reduce in one pass

#include <iostream>
#include <numeric>
#include <vector>

int main() {
    std::vector<double> a{1,2,3};
    std::vector<double> b{4,5,6};

    double dot = std::transform_reduce(a.begin(), a.end(), b.begin(), 0.0);
    std::cout << dot << "\n"; // 32
}

This example demonstrates std::transform_reduce, introduced in C++17, which combines a transformation step and a reduction step into a single, expressive algorithm. In the example, corresponding elements of the two vectors are multiplied and then summed to compute a dot product, all in one call without explicit loops. std::transform_reduce improves clarity by directly expressing the intent of “map then reduce,” enables potential parallel execution via execution policies, and can be more efficient than separate transform and accumulate passes.

Node handles

#include <iostream>
#include <map>

int main() {
    std::map<int, int> a{{1,10},{2,20}};
    std::map<int, int> b{{3,30}};

    auto nh = a.extract(2); // removes key 2 from a, keeps node
    b.insert(std::move(nh)); // inserts into b

    std::cout << "a.size=" << a.size() << " b.size=" << b.size() << "\n";
}

This example demonstrates node handle operations (extract and insert) for associative containers, introduced in C++17, which allow elements to be removed from one container and inserted into another without reallocation or rehashing. The call a.extract(2) removes the element with key 2 from map a while preserving it as a node handle that owns the underlying storage, and b.insert(std::move(nh)) transfers that node directly into map b. This feature enables efficient container-to-container transfers and advanced manipulation patterns that were previously impossible or required costly copy operations.

std::optional

#include <iostream>
#include <optional>

std::optional<int> maybe_parse(bool ok) {
    if (!ok) return std::nullopt;
    return 123;
}

int main() {
    auto v = maybe_parse(true);
    std::cout << v.value_or(-1) << "\n";
}

This example demonstrates std::optional, introduced in C++17, which represents a value that may or may not be present without resorting to sentinel values or separate error codes. The function maybe_parse returns an empty std::optional (std::nullopt) to signal failure or a populated one to signal success, and the caller safely accesses the result using value_or to provide a default. std::optional makes absence an explicit part of the type system, improving readability, correctness, and robustness in APIs that naturally model “maybe” results.

Parallel algorithms with execution policies

// Note: requires libstdc++ parallel algorithms support + often -ltbb depending on setup.
// sudo apt install libtbb-dev
// g++ -std=c++17 -O2 -pthread parallel_algorithms.cpp -ltbb -o parallel_algorithms

#include <algorithm>
#include <execution>
#include <iostream>
#include <numeric>
#include <vector>

int main() {
    std::vector<int> v(1'000'000);
    std::iota(v.begin(), v.end(), 1);

    std::for_each(std::execution::par, v.begin(), v.end(), [](int& x){ x *= 2; });
    std::cout << v[0] << " " << v.back() << "\n";
}

This example demonstrates parallel algorithms with execution policies, introduced in C++17, which allow standard algorithms to express potential parallelism without changing their fundamental structure. By passing std::execution::par to std::for_each, the program requests that the algorithm be executed in parallel across multiple threads, enabling the element-wise transformation to be distributed over available cores. The actual degree of parallelism is implementation-defined—in libstdc++ it typically requires a backend such as Intel TBB—but the execution policy cleanly separates what the algorithm does from how it is executed, making it easy to write code that can scale from sequential to parallel execution when the library and platform support it.

Parsing without memory allocation

#include <iostream>
#include <string_view>

int main() {
    std::string_view s = "ABC=123";
    auto pos = s.find('=');
    auto key = s.substr(0, pos);
    auto val = s.substr(pos + 1);
    std::cout << key << " " << val << "\n";
}

This example demonstrates std::string_view, introduced in C++17, which provides a lightweight, non-owning view into a contiguous sequence of characters. By operating on std::string_view rather than std::string, the code can slice and examine substrings (find, substr) without allocating memory or copying data, making it efficient for parsing and inspection tasks. std::string_view improves performance and expressiveness in read-only string processing while clearly conveying that the underlying character data is not owned and must outlive the view.

Polymorphic memory resources

#include <iostream>
#include <memory_resource>
#include <vector>

int main() {
    std::byte buf[1024];
    std::pmr::monotonic_buffer_resource pool(buf, sizeof(buf));
    std::pmr::vector<int> v(&pool);

    v.push_back(1);
    v.push_back(2);
    std::cout << v.size() << "\n";
}

This example demonstrates polymorphic memory resources (PMR), introduced in C++17, which decouple allocation strategy from container types. By creating a std::pmr::monotonic_buffer_resource backed by a fixed buffer and passing it to a std::pmr::vector, the container allocates memory from the provided pool rather than the global heap. This design enables fine-grained control over memory allocation, improves performance and predictability in allocation-heavy code, and allows allocation strategies to be swapped or tuned without changing container interfaces.

Prefix-sum algorithms

#include <iostream>
#include <numeric>
#include <vector>

int main() {
    std::vector<int> v{1,2,3,4};
    std::vector<int> out(v.size());

    std::inclusive_scan(v.begin(), v.end(), out.begin());
    for (int x : out) std::cout << x << " ";
    std::cout << "\n"; // 1 3 6 10
}

This example demonstrates prefix-sum algorithms with std::inclusive_scan, introduced in C++17, which compute running totals of a sequence in a single pass. The call to inclusive_scan produces an output sequence where each element is the sum of all preceding input elements up to and including the current one, yielding 1 3 6 10 for the given input. This algorithm generalizes the classic prefix-sum pattern, improves clarity by expressing intent directly, and can take advantage of parallel execution policies for improved performance on large data sets.

std::sample

#include <algorithm>
#include <iostream>
#include <random>
#include <vector>

int main() {
    std::vector<int> v{1,2,3,4,5,6,7,8,9,10};
    std::vector<int> out;
    std::mt19937 rng(123);

    std::sample(v.begin(), v.end(), std::back_inserter(out), 3, rng);
    for (int x : out) std::cout << x << " ";
    std::cout << "\n";
}

This example demonstrates std::sample, introduced in C++17, which selects a fixed-size random sample from a population without requiring the entire range to be shuffled. The algorithm draws three elements from the input vector using a supplied random number generator (std::mt19937), producing an unbiased sample while potentially examining fewer elements than a full shuffle would require. std::sample is particularly useful for statistical sampling, testing, and randomized algorithms where efficiency and clarity are important and the original ordering of the input need not be preserved.

Reader-writer synchronization

#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <thread>

int main() {
    std::shared_mutex m;
    int x = 0;

    auto reader = [&]{
        std::shared_lock<std::shared_mutex> lk(m);
        std::cout << x << "\n";
    };
    auto writer = [&]{
        std::unique_lock<std::shared_mutex> lk(m);
        x = 42;
    };

    std::thread t1(writer), t2(reader);
    t1.join(); t2.join();
}

This example demonstrates reader–writer synchronization with std::shared_mutex, introduced in C++17, which allows multiple threads to hold a shared (read-only) lock concurrently while still providing exclusive access for writers. In the example, the reader acquires a std::shared_lock to safely read the shared variable, while the writer acquires a std::unique_lock to perform an exclusive update. This pattern improves concurrency and scalability in read-heavy workloads by permitting parallel reads without sacrificing correctness when writes occur.

Scoped lock

#include <iostream>
#include <mutex>

int main() {
    std::mutex a, b;
    {
        std::scoped_lock lock(a, b); // locks both safely
        std::cout << "locked\n";
    }
}

This example demonstrates std::scoped_lock, introduced in C++17, which provides a safe and concise way to lock multiple mutexes simultaneously. By constructing a single std::scoped_lock with both mutexes, the program acquires the locks using a deadlock-avoidance strategy defined by the standard and automatically releases them when the lock object goes out of scope. This RAII-based mechanism simplifies correct mutex handling, reduces boilerplate, and helps prevent common concurrency errors such as deadlocks and forgotten unlocks.

std::string_view

#include <iostream>
#include <string_view>

void show(std::string_view s) {
    std::cout << s << " (" << s.size() << ")\n";
}

int main() {
    const char* c = "hello";
    show(c);
    show("world");
}

This example demonstrates std::string_view as a function parameter, introduced in C++17, highlighting its role as an efficient, non-owning abstraction for read-only string data. The function show accepts a std::string_view, allowing it to be called with different string representations—such as string literals or C-style strings—without copying or allocating memory. This makes std::string_view ideal for APIs that only need to observe character sequences, improving performance and flexibility while clearly communicating that the function does not take ownership of the underlying data.

Type-safe tagged unions

#include <iostream>
#include <string>
#include <variant>

int main() {
    std::variant<int, std::string> v = "hi";
    std::visit([](const auto& x){ std::cout << x << "\n"; }, v);
}

This example demonstrates type-safe tagged unions with std::variant, introduced in C++17, which allow a variable to hold one of several specified types at a time while tracking the active alternative. The program initializes a std::variant<int, std::string> with a string and then uses std::visit together with a generic lambda to apply an operation to the currently held value without unsafe casts. std::variant provides a robust alternative to unions and polymorphism when modeling sum types, enabling expressive and type-safe handling of heterogeneous data.

C++20

Language features

Concepts

// g++ -std=c++20 concepts.cpp

#include <concepts>
#include <iostream>

template<class T>
requires std::integral<T>
T twice(T x) { return x + x; }

int main() {
    std::cout << twice(21) << "\n";
}

This example demonstrates concepts, introduced in C++20, which provide a direct and expressive way to specify semantic constraints on template parameters. The requires std::integral<T> clause restricts the function template twice to integral types only, ensuring that invalid uses are rejected with clear, readable compiler diagnostics. Concepts improve template readability, make generic code self-documenting, and replace many ad-hoc static_assert or SFINAE techniques with a cleaner, more declarative constraint mechanism.

Coroutines

// g++ -std=c++20 coroutines_as_tiny_generator.cpp

#include <coroutine>
#include <iostream>

struct Gen {
    struct promise_type {
        int value{};
	Gen get_return_object() { return Gen{std::coroutine_handle<promise_type>::from_promise(*this)}; }
	std::suspend_always initial_suspend() { return {}; }
	std::suspend_always final_suspend() noexcept { return {}; }
	std::suspend_always yield_value(int v) { value = v; return{}; }
	void return_void() {}
	void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle<promise_type> h;
    explicit Gen(std::coroutine_handle<promise_type> h_) : h(h_) {}
    Gen(Gen&& o) noexcept : h(o.h) { o.h = {}; }
    ~Gen() { if (h) h.destroy(); }

    bool next() { if (!h || h.done()) return false; h.resume(); return !h.done(); }
    int value() const { return h.promise().value; }
};

Gen counter() {
    for (int i = 1; i <= 3; ++i) co_yield i;
}

int main() {
    auto g = counter();
    while (g.next()) std::cout << g.value() << " ";
    std::cout << "\n";
}

This example demonstrates coroutines, introduced in C++20, which provide language-level support for writing suspendable and resumable functions. The function counter uses co_yield to produce a sequence of values lazily, while the Gen type defines the required promise_type and coroutine handle management to control suspension, resumption, and cleanup. Coroutines allow asynchronous workflows, generators, and cooperative multitasking to be expressed in a direct, linear style, separating control flow from scheduling and enabling powerful abstractions such as lazy sequences and async operations with minimal overhead.

Designated initializers

// g++ -std=c++20 designated_initializers_c_style.cpp

#include <iostream>

struct P { int x; int y; };

int main() {
    P p{ .x = 1, .y = 2 };
    std::cout << p.x << " " << p.y << "\n";
}

This example demonstrates designated initializers, introduced in C++20, which allow aggregate members to be initialized by name rather than by position. By writing P p{ .x = 1, .y = 2 };, the code explicitly associates each value with the corresponding data member, improving readability and reducing the risk of errors when the order of members changes. This feature, inspired by C99, makes aggregate initialization clearer and more robust, especially for structures with multiple fields or when only a subset of members needs to be initialized.

Standard library enhancements

std::atomic_ref

// Compile with g++ -std=c++20 ...

#include <atomic>
#include <iostream>

int main() {
    int x = 0;
    std::atomic_ref<int> ax(x);
    ax.fetch_add(5, std::memory_order_relaxed);
    std::cout << x << "\n";
}

This example demonstrates std::atomic_ref, introduced in C++20, which allows atomic operations to be performed on an existing object without requiring it to be declared as an atomic type. By wrapping the ordinary integer x in a std::atomic_ref, the code can safely apply atomic operations such as fetch_add while leaving the object’s original type unchanged. This feature is particularly useful when retrofitting atomic access onto legacy data structures or interfacing with memory shared across threads, providing fine-grained control over concurrency with minimal structural changes.

Bit manipulation utilities

// Compile with g++ -std=c++20 ...

#include <bit>
#include <cstdint>
#include <iostream>

int main() {
    std::uint32_t x = 0b10110100u;
    std::cout << std::popcount(x) << "\n";
}

This example demonstrates bit-manipulation utilities from <bit>, introduced in C++20, specifically the function std::popcount, which counts the number of set bits (ones) in an unsigned integer. By operating on fixed-width integer types such as std::uint32_t, std::popcount provides a portable, expressive alternative to platform-specific intrinsics for common bit-level operations. This feature improves code clarity and correctness while allowing implementations to map the operation to efficient hardware instructions when available.

Cheap source site info

// Compile with g++ -std=c++20 ...

#include <iostream>
#include <source_location>

void log(const char* msg,
         std::source_location loc = std::source_location::current()) {
    std::cout << loc.file_name() << ":" << loc.line() << " " << msg << "\n";
}

int main() {
    log("hello");
}

This example demonstrates std::source_location, introduced in C++20, which allows functions to obtain information about the call site—such as file name, line number, and function name—without relying on macros. By using std::source_location::current() as a default argument, the log function automatically captures the location where it is invoked, not where it is defined. This feature enables more robust and maintainable logging, diagnostics, and error-reporting facilities by providing precise contextual information in a type-safe, standard way.

C++20 calendar and civil-time extensions to <chrono>

// Compile with g++ -std=c++20 ...

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;

    year_month_day d{year{2025}, month{12}, day{26}};
    std::cout << int(d.year()) << "-" << unsigned(d.month()) << "-" << unsigned(d.day()) << "\n";
}

This example demonstrates the C++20 calendar and civil-time extensions to <chrono>, which introduce strongly typed date components such as year, month, day, and the composite type year_month_day. By constructing a calendar date using these types, the code represents civil dates explicitly and safely, avoiding common errors associated with raw integers or ad-hoc date handling. These additions make date and time manipulation more expressive, self-documenting, and less error-prone, and they integrate seamlessly with the existing <chrono> time-point and duration framework.

Cooperative cancellation

// Compile with g++ -std=c++20 ...

#include <chrono>
#include <iostream>
#include <thread>

int main() {
    std::jthread t([](std::stop_token st) {
        while (!st.stop_requested()) {
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
        std::cout << "stopped\n";
    });

    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    t.request_stop();
}

This example demonstrates std::jthread and cooperative cancellation with std::stop_token, introduced in C++20, which simplify safe thread management and shutdown. Unlike std::thread, a std::jthread automatically joins on destruction, preventing accidental program termination due to unjoined threads, and provides an integrated cancellation mechanism via std::stop_token. In the example, the worker thread periodically checks whether a stop has been requested and exits cleanly when request_stop() is called, illustrating a structured, RAII-based approach to concurrency that reduces boilerplate and common threading errors.

Counting semaphore

// Compile with g++ -std=c++20 ...

#include <chrono>
#include <iostream>
#include <semaphore>
#include <thread>

int main() {
    std::counting_semaphore<1> sem(0);

    std::thread t([&]{
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        sem.release();
    });

    sem.acquire();
    std::cout << "go\n";
    t.join();
}

This example demonstrates std::counting_semaphore, introduced in C++20, which provides a low-level synchronization primitive for controlling access to shared resources across threads. The semaphore is initialized with a count of zero, causing the main thread to block on acquire() until another thread signals availability by calling release(). Counting semaphores are useful for coordinating producer–consumer relationships, limiting concurrency, and expressing simple signaling patterns in concurrent programs without the overhead of mutexes and condition variables.

Container erasure helpers: std::erase and std::erase_if

// Compile with g++ -std=c++20 ...

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{1,2,3,4,5,6};
    std::erase_if(v, [](int x){ return x % 2 == 0; }); // remove evens
    for (int x : v) std::cout << x << " ";
    std::cout << "\n";
}

This example demonstrates container erasure helpers std::erase and std::erase_if, introduced in C++20, which provide a concise and safe way to remove elements from standard containers. The call to std::erase_if removes all elements that satisfy the given predicate—in this case, even numbers—without requiring the verbose and error-prone “erase–remove” idiom previously needed with sequence containers. These helpers improve readability, reduce boilerplate, and make element removal operations more expressive and less susceptible to common mistakes.

Format

// Compile with g++ -std=c++20 ...

#include <format>
#include <iostream>

int main() {
    std::cout << std::format("id={}, px={:.2f}\n", 7, 12.345);
}

This example demonstrates std::format, introduced in C++20, which provides modern, type-safe string formatting facilities inspired by Python’s format syntax. The function constructs formatted output by combining a format string with strongly typed arguments, eliminating the need for stream insertion chains or unsafe printf-style formatting. std::format improves readability, reduces formatting errors, and allows precise control over presentation—such as numeric precision—while remaining efficient and fully integrated with the C++ type system.

std::latch

// Compile with g++ -std=c++20 ...

#include <iostream>
#include <latch>
#include <thread>

int main() {
    std::latch done(3);

    auto worker = [&](int i) {
        // ... do work ...
        std::cout << "w" << i << " ";
        done.count_down();
    };

    std::thread t1(worker, 1), t2(worker, 2), t3(worker, 3);
    done.wait(); // wait for all workers
    std::cout << "\nall done\n";

    t1.join(); t2.join(); t3.join();
}

This example demonstrates std::latch, introduced in C++20, a one-shot synchronization primitive used to coordinate multiple threads. The latch is initialized with a count representing the number of events or tasks to wait for, and each worker thread calls count_down() when it finishes its work. The main thread blocks on wait() until the count reaches zero, at which point execution continues, making std::latch ideal for simple “wait until all tasks complete” scenarios without the complexity of condition variables or repeated synchronization.

std::barrier

// Compile with g++ -std=c++20 ...

#include <barrier>
#include <iostream>
#include <thread>

int main() {
    std::barrier sync(2, []{ std::cout << "phase done\n"; });

    std::thread t([&]{ std::cout << "t1\n"; sync.arrive_and_wait(); });
    std::cout << "t2\n";
    sync.arrive_and_wait();

    t.join();
}

This example demonstrates std::barrier, introduced in C++20, which provides a reusable synchronization point for coordinating phases of work across multiple threads. Unlike std::latch, a barrier can be used repeatedly, allowing threads to rendezvous at the end of each phase before proceeding to the next. In the example, both threads call arrive_and_wait(), and once all participants have arrived, the optional completion function is executed and the barrier automatically resets, making std::barrier well suited for iterative, phase-based parallel algorithms.

The Ranges library algorithms

// Compile with g++ -std=c++20 ...

#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> v{3,1,2};
    std::ranges::sort(v);
    for (int x : v) std::cout << x << " ";
    std::cout << "\n";
}

This example demonstrates the Ranges library algorithms, introduced in C++20, which provide a modernized interface to the standard algorithms with improved composability and safety. By calling std::ranges::sort(v), the algorithm operates directly on the container without explicitly passing iterator pairs, reducing verbosity and the risk of mismatched ranges. Ranges algorithms integrate naturally with views and projections, encourage more expressive code, and make algorithm usage clearer by more closely matching the programmer’s intent.

Ranges views and lazy evaluation

// Compile with g++ -std=c++20 ...

#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> v{1,2,3,4,5,6};
    auto evens = v | std::views::filter([](int x){ return x % 2 == 0; });
    for (int x : evens) std::cout << x << " ";
    std::cout << "\n";
}

This example demonstrates ranges views and lazy evaluation, introduced in C++20, specifically the use of std::views::filter to create a composable, non-owning view over a container. The expression v | std::views::filter(...) constructs a filtered view that presents only the elements satisfying the predicate, without copying data or eagerly producing a new container. Elements are evaluated lazily as the view is iterated, which improves efficiency and expressiveness. Ranges views enable a functional, pipeline-style approach to data processing in C++, making transformations clearer, safer, and more modular than traditional iterator-based code.

Safe bit reinterpretation

// Compile with g++ -std=c++20 ...

#include <bit>
#include <cstdint>
#include <iostream>

int main() {
    float f = 1.0f;
    std::uint32_t u = std::bit_cast<std::uint32_t>(f);
    std::cout << u << "\n";
}

This example demonstrates std::bit_cast, introduced in C++20, which provides a safe, well-defined way to reinterpret the bit representation of an object as another trivially copyable type of the same size. In the example, the bit pattern of a float is copied into a std::uint32_t without invoking undefined behavior, unlike traditional techniques such as reinterpret_cast or pointer aliasing. std::bit_cast makes low-level operations like serialization, hashing, and numeric inspection both explicit and portable, while allowing compilers to generate efficient code equivalent to a raw bit copy.

std::span

// Compile with g++ -std=c++20 ...

#include <iostream>
#include <span>
#include <vector>

void print(std::span<const int> s) {
    for (int x : s) std::cout << x << " ";
    std::cout << "\n";
}

int main() {
    std::vector<int> v{1,2,3,4};
    print(v);
}

This example demonstrates std::span, introduced in C++20, which provides a lightweight, non-owning view over a contiguous sequence of elements. By accepting a std::span<const int> parameter, the print function can operate on any compatible contiguous container—such as a std::vector, array, or raw pointer with a size—without copying data or committing to a specific container type. std::span improves API flexibility, safety, and clarity by pairing a pointer with a size in a single abstraction, making bounds-aware, read-only access to sequences both efficient and expressive.

C++23

Language features

Deducing “this”

// g++ -std=c++23 deducing_this.cpp

#include <iostream>
#include <utility>

struct Acc {
    int x = 0;

    template <class Self>
    auto&& get(this Self&& self) {
        return std::forward<Self>(self).x; // preserves value category
    }
};

int main() {
    Acc a{42};
    a.get() = 7;
    std::cout << a.x << "\n";
}

This example demonstrates explicit object parameters (often called “deducing this”), introduced in C++23, which allow member functions to treat the implicit this object as an explicit, deduced function parameter. By writing auto&& get(this Self&& self), the member function becomes a template over the value category of the object it is called on, enabling perfect forwarding of *this. In the example, the function preserves whether the object is an lvalue or rvalue, allowing get() to return a correctly qualified reference to x. This feature simplifies the implementation of ref-qualified overloads, improves generic member functions, and makes value-category–aware APIs more expressive and maintainable.

Standard library enhancements

std::expected

// g++ -std=c++23 expected.cpp

#include <expected>
#include <iostream>
#include <string>

std::expected<int, std::string> parse_nonneg(const std::string & s) {
    try {
        int x = std::stoi(s);
	if (x < 0) return std::unexpected("negative");
	return x;
    } catch (...) {
        return std::unexpected("not an int");
    }
}

int main() {
    auto r = parse_nonneg("12");
    if (r) std::cout << *r << "\n";
    else   std::cout << "err: " << r.error() << "\n";
}

This example demonstrates std::expected, introduced in C++23, which represents the result of a computation that may either succeed with a value or fail with an error. The function parse_nonneg returns a std::expected<int, std::string>, using std::unexpected to explicitly encode error conditions such as invalid input or negative values without throwing exceptions. At the call site, the result can be checked and handled in a straightforward, type-safe manner. std::expected enables clear, explicit error handling and is particularly well suited for performance-sensitive or library code where exceptions are undesirable or too heavy-weight.

C++26

Language features

Static (compile-time) reflection

#include <iostream>
#include <string>
#include <type_traits>

// Feature-test macros are the standard way to probe for new features

struct Person {
    int id;
    std::string name;
};

// Fallback: manual printing (works everywhere)
void print_person_fallback(const Person& p) {
    std::cout << "fallback Person{id=" << p.id << ", name=" << p.name << "}\n";
}

int main() {
    Person p{1, "Ada"};

    // Relection path (C++26)
    // GCC reflection support is still evolving; when it exists, it will typically be behind
    // a feature-test macro and/or require -std=c++2c
#if defined(__cpp_reflection) // name may vary as implementations evolve
    // PSEUDOCODE SHAPE (will compile once GCC has it):
    //
    // using namespace std::meta;
    // constexpr auto info = reflexpr(Person);
    // for...(member: members_of(info)) {
    //     std::cout << name_of(member) << "=" << p.*pointer_to_member(member) << "\n";
    // }
    // For now, print something so the file stays buildable:
    std::cout << "Reflection is available on this compiler build.\n";
    print_person_fallback(p);
#else
    // Portable path (works on released GCC today)
    std::cout << "Reflection is not available; using fallback.\n";
    print_person_fallback(p);
#endif
}

This example demonstrates static (compile-time) reflection as proposed for C++26, together with the use of feature-test macros to write forward-compatible code. The program conditionally checks for the presence of a reflection feature macro (such as __cpp_reflection) to determine whether the compiler supports compile-time introspection of types. When available, static reflection would allow the program to enumerate members of the Person struct and access them generically at compile time, enabling powerful patterns like automatic serialization, logging, or structural algorithms without boilerplate. Until such support is present in GCC, the code safely falls back to a conventional implementation, illustrating how feature-test macros enable experimental adoption of new language features while preserving portability and buildability on current toolchains.


Leave a Reply

Your email address will not be published. Required fields are marked *