LOADING

加载过慢请开启缓存 浏览器默认开启

C++ Unleashed: C++20 Features

C++ Unleashed: From Zero to Hero

Previous chapter: Advanced Template Programming

Go to Table of Contents

C++20 Features

C++20 is a significant update to the C++ programming language, introducing a plethora of new features and enhancements that aim to make C++ more powerful, expressive, and easier to use. This chapter explores the most impactful features of C++20, including Concepts, Ranges, Coroutines, Modules, the Three-Way Comparison Operator (<=>), std::span, and other notable additions to the Standard Library. By understanding and leveraging these features, you can write more efficient, readable, and maintainable C++ code.

Table of Contents for This Chapter

  1. Introduction to C++20
  2. Concepts
  3. Ranges
  4. Coroutines
  5. Modules
  6. Three-Way Comparison Operator (<=>)
  7. std::span
  8. Other Notable C++20 Features
  9. Best Practices for Using C++20 Features
  10. Practical Examples
  11. Summary

Introduction to C++20

Overview of C++20

C++20 is a major milestone in the evolution of the C++ language, building upon the foundation laid by previous standards like C++11, C++14, C++17, and C++23. It introduces a range of features that enhance the language’s expressiveness, performance, and usability. Some of the key highlights include:

  • Concepts: Compile-time constraints for template parameters.
  • Ranges: Improved and more expressive ways to work with sequences of elements.
  • Coroutines: Native support for asynchronous programming.
  • Modules: A new way to organize and compile code, improving compile times and encapsulation.
  • Three-Way Comparison Operator (<=>): Simplifies the implementation of comparison operators.
  • std::span: A lightweight object that can reference a sequence of elements without owning them.
  • Various enhancements to the Standard Library: Including std::format, enhanced std::chrono, and more.

Why Upgrade to C++20?

Adopting C++20 allows developers to:

  • Write More Expressive Code: Features like Concepts and Ranges enable more readable and maintainable code.
  • Improve Performance: Coroutines and Modules can lead to more efficient programs.
  • Enhance Type Safety: Concepts provide stronger compile-time checks.
  • Simplify Asynchronous Programming: Coroutines make writing asynchronous code more straightforward.

Before proceeding, ensure that your development environment supports C++20. While most modern compilers like GCC (from version 10), Clang (from version 10), and MSVC (from Visual Studio 2019 version 16.8) offer varying levels of C++20 support, it’s important to note that module programming in C++20 does not yet have high support across these three major mainstream compilers. This means that while Modules are a powerful feature, their adoption may be limited until compiler support becomes more mature and standardized.


Concepts

What Are Concepts?

Concepts are a feature introduced in C++20 that provide a way to specify constraints on template parameters. They allow developers to define requirements that types must meet to be used with certain templates, enhancing code clarity and compile-time error messages.

Benefits of Using Concepts

  • Improved Readability: Clearly states the requirements for template parameters.
  • Better Error Messages: Compiler errors are more informative when template constraints are not met.
  • Code Reusability and Safety: Ensures that templates are used with appropriate types.

Defining and Using Concepts

Defining a Concept

A concept is defined using the concept keyword, followed by a boolean expression that specifies the constraints.

Example: Defining a Concept for Arithmetic Types

#include <concepts>
#include <iostream>

// Define a concept named 'Arithmetic' that checks if a type is integral or floating-point
template<typename T>
concept Arithmetic = std::is_integral_v<T> || std::is_floating_point_v<T>;

Using Concepts in Templates

Concepts can be used to constrain template parameters in various ways:

  1. As a requires Clause
  2. Using template Parameter Syntax
  3. Inline Constraints

Example: Using Concepts with a Function Template

#include <concepts>
#include <iostream>

// Define the 'Arithmetic' concept
template<typename T>
concept Arithmetic = std::is_integral_v<T> || std::is_floating_point_v<T>;

// Function that adds two arithmetic types
Arithmetic auto add(Arithmetic auto a, Arithmetic auto b) {
    return a + b;
}

int main() {
    std::cout << add(5, 10) << std::endl;       // Outputs: 15
    std::cout << add(3.14, 2.71) << std::endl; // Outputs: 5.85
    // std::cout << add("Hello", "World") << std::endl; // Compilation error
    return 0;
}

Output:

15
5.85

Explanation:

  • The add function is constrained to accept only types that satisfy the Arithmetic concept.
  • Attempting to call add with non-arithmetic types (e.g., strings) results in a clear compilation error.

Advanced Concepts

Concepts can express more complex constraints, including multiple requirements and even custom behaviors.

Example: Defining a Concept for Equality Comparable Types

#include <concepts>
#include <iostream>

// Define a concept that checks if a type supports equality comparison
template<typename T>
concept EqualityComparable = requires(T a, T b) {
    { a == b } -> std::convertible_to<bool>;
};

// Function that checks if two objects are equal
bool areEqual(EqualityComparable auto a, EqualityComparable auto b) {
    return a == b;
}

int main() {
    std::cout << std::boolalpha;
    std::cout << areEqual(5, 5) << std::endl;       // true
    std::cout << areEqual(5, 10) << std::endl;      // false
    std::cout << areEqual("Hello", "World") << std::endl; // false
    return 0;
}

Output:

true
false
false

Explanation:

  • The EqualityComparable concept ensures that the types used with areEqual support the == operator and that it returns a type convertible to bool.

Standard Library Concepts

C++20 introduces several predefined concepts in the <concepts> header, such as:

  • std::integral: Types that represent integral numbers.
  • std::floating_point: Types that represent floating-point numbers.
  • std::same_as: Checks if two types are the same.
  • std::derived_from: Checks if one type is derived from another.
  • std::convertible_to: Checks if one type is convertible to another.

Example: Using Standard Library Concepts

#include <concepts>
#include <iostream>

// Function that only accepts types derived from Base
struct Base {};
struct Derived : Base {};
struct Unrelated {};

template<typename T>
requires std::derived_from<T, Base>
void process(const T& obj) {
    std::cout << "Processing object derived from Base." << std::endl;
}

int main() {
    Derived d;
    process(d); // Valid
    // Unrelated u;
    // process(u); // Compilation error
    return 0;
}

Output:

Processing object derived from Base.

Ranges

What Are Ranges?

Ranges are a new addition in C++20 that provide a more expressive and intuitive way to work with sequences of elements. They build upon the existing iterator-based approach, offering a more declarative style of programming that can lead to cleaner and more readable code.

Benefits of Using Ranges

  • Improved Readability: Chain operations in a natural, sequential manner.
  • Lazy Evaluation: Operations are executed only when necessary, enhancing performance.
  • Composability: Easily combine multiple range operations without creating intermediate containers.

Core Components of Ranges

  1. Views: Lightweight, non-owning, and composable views into data.
  2. Actions: Operations that can be performed on ranges, such as filtering, transforming, and iterating.
  3. Range Adaptors: Tools to modify ranges on-the-fly without altering the original data.

Using Range-Based Algorithms

C++20 introduces new algorithms in the <algorithm> header that work seamlessly with ranges.

Example: Using std::ranges::for_each

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

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // Apply a lambda function to each element using ranges
    std::ranges::for_each(numbers, [](int n) {
        std::cout << n * n << " ";
    });

    std::cout << std::endl;
    return 0;
}

Output:

1 4 9 16 25 

Range Adaptors

Range adaptors allow you to create modified views of existing ranges without copying data.

Example: Using std::views::filter and std::views::transform

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

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};

    // Create a view that filters even numbers and then squares them
    auto processed = numbers 
                   | std::views::filter([](int n) { return n % 2 == 0; })
                   | std::views::transform([](int n) { return n * n; });

    // Iterate over the processed view
    for(auto n : processed) {
        std::cout << n << " "; // Outputs: 4 16 36 
    }

    std::cout << std::endl;
    return 0;
}

Output:

4 16 36 

Explanation:

  • std::views::filter: Creates a view that includes only even numbers.
  • std::views::transform: Transforms each element by squaring it.
  • Chaining Adaptors: Allows for a declarative and readable sequence of operations.

Range-Based Containers

Most standard containers can be used with ranges, providing a seamless integration of range-based algorithms and adaptors.

Example: Using Ranges with std::vector

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

int main() {
    std::vector<std::string> words = {"apple", "banana", "cherry", "date", "elderberry"};

    // Find all words that start with the letter 'b' or 'c' and convert them to uppercase
    auto processed = words 
                   | std::views::filter([](const std::string& s) {
                       return !s.empty() && (s[0] == 'b' || s[0] == 'c');
                   })
                   | std::views::transform([](std::string s) {
                       for(auto& ch : s) ch = std::toupper(ch);
                       return s;
                   });

    // Display the processed words
    for(auto& word : processed) {
        std::cout << word << " "; // Outputs: BANANA CHERRY 
    }

    std::cout << std::endl;
    return 0;
}

Output:

BANANA CHERRY 

Combining Ranges with Algorithms

Ranges can be combined with traditional algorithms for more powerful data processing.

Example: Using std::ranges::sort with a View

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

int main() {
    std::vector<int> numbers = {5, 3, 1, 4, 2};

    // Create a view that excludes the first element
    auto view = numbers | std::views::drop(1);

    // Since views are non-owning and non-modifiable by default, we need to copy to a new container
    auto sorted_view = std::vector<int>(view.begin(), view.end());

    std::ranges::sort(sorted_view);

    // Display the sorted view
    for(auto n : sorted_view) {
        std::cout << n << " "; // Outputs: 2 3 4 5 
    }

    std::cout << std::endl;
    return 0;
}

Output:

2 3 4 5 

Note: The original numbers vector remains unchanged.


Coroutines

What Are Coroutines?

Coroutines are a feature introduced in C++20 that enable functions to suspend and resume execution without blocking the thread. They provide a powerful mechanism for writing asynchronous and lazy-evaluated code in a natural and readable manner.

Benefits of Using Coroutines

  • Simplified Asynchronous Code: Write asynchronous operations without complex callback mechanisms.
  • Improved Readability: Code resembles synchronous code, making it easier to understand.
  • Efficiency: Coroutines can reduce overhead by avoiding unnecessary thread creation.

Understanding Coroutine Components

  1. co_await: Suspends the coroutine until the awaited operation is complete.
  2. co_yield: Produces a value to the caller and suspends the coroutine.
  3. co_return: Completes the coroutine and optionally returns a value.

Basic Coroutine Example

Example: Simple Coroutine that Yields Values

#include <coroutine>
#include <iostream>
#include <optional>

// Define a generator class
template<typename T>
struct Generator {
    struct promise_type {
        std::optional<T> current_value;

        Generator get_return_object() {
            return Generator{ 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(T value) {
            current_value = value;
            return {};
        }

        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle<promise_type> handle;

    Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~Generator() { if(handle) handle.destroy(); }

    std::optional<T> next() {
        if(!handle.done()) {
            handle.resume();
            return handle.promise().current_value;
        }
        return std::nullopt;
    }
};

// Coroutine function that generates numbers from 1 to 5
Generator<int> generateNumbers() {
    for(int i = 1; i <= 5; ++i) {
        co_yield i;
    }
}

int main() {
    auto gen = generateNumbers();
    while(auto val = gen.next()) {
        std::cout << "Generated: " << *val << std::endl;
    }
    return 0;
}

Output:

Generated: 1
Generated: 2
Generated: 3
Generated: 4
Generated: 5

Explanation:

  • Generator Class: Manages the coroutine’s state and provides a next method to retrieve generated values.
  • promise_type: Defines how the coroutine behaves, including how it yields values.
  • generateNumbers Coroutine: Yields numbers from 1 to 5 using co_yield.

Asynchronous Coroutines with std::future

Coroutines can also be used to perform asynchronous operations that return std::future objects.

Example: Asynchronous Coroutine that Returns a Future

#include <coroutine>
#include <future>
#include <iostream>

// Awaitable type that represents a ready future
struct ReadyFuture {
    std::future<int> fut;

    bool await_ready() const noexcept { return true; }
    void await_suspend(std::coroutine_handle<>) noexcept {}
    int await_resume() { return fut.get(); }
};

// Coroutine function that returns a future
std::future<int> asyncAdd(int a, int b) {
    co_return a + b;
}

int main() {
    auto fut = asyncAdd(10, 20);
    std::cout << "Sum: " << fut.get() << std::endl; // Outputs: Sum: 30
    return 0;
}

Output:

Sum: 30

Explanation:

  • asyncAdd Coroutine: Returns a std::future<int> that will hold the result of the addition.
  • Awaiting the Future: In this simple example, co_return automatically creates a ready future.

Best Practices with Coroutines

  1. Understand the Coroutine Lifecycle: Be aware of how coroutines are created, suspended, and destroyed.
  2. Manage Resource Lifetimes: Ensure that resources used within coroutines remain valid across suspensions.
  3. Use Coroutines for Asynchronous Tasks: Ideal for I/O operations, event handling, and other asynchronous workflows.
  4. Combine with Other C++20 Features: Leverage Concepts and Ranges alongside Coroutines for more powerful abstractions.

Modules

What Are Modules?

Modules are a major feature introduced in C++20 that aim to replace the traditional preprocessor-based include system. They provide a more robust, efficient, and scalable way to organize and compile C++ code by encapsulating code into named units with explicit interfaces.

Benefits of Using Modules

  • Faster Compilation: Reduce compile times by avoiding redundant processing of header files.
  • Improved Encapsulation: Clearly define interfaces and hide implementation details.
  • Better Dependency Management: Avoid issues like the infamous “include hell” and circular dependencies.
  • Enhanced Reliability: Eliminate macro-related bugs and improve code safety.

Defining and Importing Modules

Creating a Module Interface

A module interface file defines the public interface of a module. It uses the export module declaration.

Example: Defining a Module Interface (math.ixx)

// math.ixx - Module Interface File

export module math;

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

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

Importing a Module

To use a module in your code, use the import keyword followed by the module name.

Example: Using the math Module (main.cpp)

// main.cpp

import math;
#include <iostream>

int main() {
    int sum = add(5, 3);
    int product = multiply(5, 3);

    std::cout << "Sum: " << sum << std::endl;         // Outputs: Sum: 8
    std::cout << "Product: " << product << std::endl; // Outputs: Product: 15

    return 0;
}

Compilation Steps:

  1. Compile the Module Interface
    g++ -std=c++20 -fmodules-ts -c math.ixx -o math.o
    
  2. Compile the Main Program
    g++ -std=c++20 main.cpp math.o -o main
    
  3. Run the Program
    ./main
    

Output:

Sum: 8
Product: 15

Module Partitioning

Modules can be divided into partitions to manage large codebases more effectively. Partitions allow splitting a module’s interface and implementation into separate files.

Example: Module with Partitions

  1. Module Interface (graphics.ixx)

    // graphics.ixx - Module Interface
    
    export module graphics;
    
    export void drawCircle();
    export void drawSquare();
    
    export import graphics.details; // Importing partition
    
  2. Module Partition (graphics_details.ixx)

    // graphics_details.ixx - Module Partition
    
    module graphics.details;
    
    void drawCircle() {
        // Implementation of drawing a circle
    }
    
    void drawSquare() {
        // Implementation of drawing a square
    }
    
  3. Using the Module (main.cpp)

    // main.cpp
    
    import graphics;
    #include <iostream>
    
    int main() {
        drawCircle();
        drawSquare();
        std::cout << "Shapes drawn successfully." << std::endl;
        return 0;
    }
    

Compilation Steps:

  1. Compile the Module Interface and Partition
    g++ -std=c++20 -fmodules-ts -c graphics.ixx -o graphics.o
    g++ -std=c++20 -fmodules-ts -c graphics_details.ixx -o graphics_details.o
    
  2. Compile the Main Program
    g++ -std=c++20 main.cpp graphics.o graphics_details.o -o main
    
  3. Run the Program
    ./main
    

Output:

Shapes drawn successfully.

Compiler Support and Considerations

While Modules are a powerful feature, module programming in C++20 does not yet have high support across the three major mainstream compilers:

  • GCC: Partial support starting from version 10, with ongoing improvements.
  • Clang: Partial support with significant progress in recent versions.
  • MSVC: More mature support compared to GCC and Clang, but still evolving.

Implications:

  • Portability: Code utilizing Modules may face compatibility issues across different compiler versions.
  • Adoption: Given the limited support, it’s advisable to use Modules judiciously, especially in projects targeting multiple compiler platforms.
  • Future Proofing: As compiler support matures, adopting Modules will become more beneficial. Staying informed about compiler updates is essential.

Recommendation:

  • Evaluate Necessity: Assess whether the benefits of Modules outweigh the current limitations in compiler support for your project.
  • Fallback Mechanisms: Consider providing traditional header-based includes as a fallback when Modules are not supported.
  • Stay Updated: Keep track of compiler releases and updates to leverage improved Module support as it becomes available.

Best Practices with Modules

  1. Organize Code into Logical Modules: Group related functionalities into separate modules for better maintainability.
  2. Minimize Module Interfaces: Expose only what is necessary to reduce dependencies and improve encapsulation.
  3. Avoid Circular Dependencies: Design modules to prevent circular imports, which can lead to compilation issues.
  4. Leverage Module Partitions: Use partitions to manage large modules by splitting them into manageable pieces.
  5. Provide Fallbacks: Ensure that your codebase can compile without Modules if targeting compilers with limited support.
  6. Document Module Interfaces: Clearly document the public interface of each module to aid developers in understanding dependencies and usage.

Three-Way Comparison Operator (<=>)

What Is the Three-Way Comparison Operator?

The three-way comparison operator, also known as the “spaceship operator” (<=>), was introduced in C++20 to simplify the process of implementing comparison operators. It provides a unified way to perform all relational comparisons (<, <=, >, >=) and equality comparisons (==, !=).

Benefits of Using the <=> Operator

  • Simplifies Operator Definitions: Reduces boilerplate code by handling multiple comparisons in one operator.
  • Consistent Comparison Logic: Ensures consistent behavior across different comparison operators.
  • Supports Defaulted Comparisons: Allows the compiler to generate default comparison logic for user-defined types.

Using the <=> Operator

Implementing the <=> Operator

To implement the three-way comparison operator, define the operator<=> in your class. You can also default the operator to let the compiler generate it automatically.

Example: Implementing <=> in a Point Class

#include <compare>
#include <iostream>

struct Point {
    int x;
    int y;

    // Implement the three-way comparison operator
    auto operator<=>(const Point&) const = default;
};

int main() {
    Point p1{1, 2};
    Point p2{1, 3};

    if(p1 < p2) {
        std::cout << "p1 is less than p2" << std::endl;
    } else if(p1 > p2) {
        std::cout << "p1 is greater than p2" << std::endl;
    } else {
        std::cout << "p1 is equal to p2" << std::endl;
    }

    return 0;
}

Output:

p1 is less than p2

Explanation:

  • Defaulted <=> Operator: By using = default, the compiler automatically generates the comparison logic based on the member variables.
  • Comparison Order: The comparison starts with the first member (x), and if they are equal, it proceeds to the next member (y).

Customizing the <=> Operator

You can provide custom comparison logic by implementing the operator<=> manually.

Example: Custom <=> Implementation

#include <compare>
#include <iostream>

struct Person {
    std::string name;
    int age;

    // Custom three-way comparison operator
    std::partial_ordering operator<=>(const Person& other) const {
        if(auto cmp = name <=> other.name; cmp != 0) {
            return cmp;
        }
        return age <=> other.age;
    }

    bool operator==(const Person& other) const = default;
};

int main() {
    Person alice{"Alice", 30};
    Person bob{"Bob", 25};
    Person anotherAlice{"Alice", 30};

    std::cout << std::boolalpha;
    std::cout << "alice == bob: " << (alice == bob) << std::endl;               // false
    std::cout << "alice < bob: " << (alice < bob) << std::endl;                 // true
    std::cout << "alice == anotherAlice: " << (alice == anotherAlice) << std::endl; // true

    return 0;
}

Output:

alice == bob: false
alice < bob: true
alice == anotherAlice: true

Explanation:

  • Custom Logic: First compares name, and if equal, compares age.
  • Partial Ordering: Uses std::partial_ordering to handle cases where comparison might not be strict.

Best Practices with the <=> Operator

  1. Leverage Defaulted Comparisons: Use = default whenever possible to reduce boilerplate code.
  2. Ensure Consistency: Make sure that all members used in comparison are consistent with the intended ordering logic.
  3. Understand Comparison Categories: Use appropriate comparison categories (std::strong_ordering, std::weak_ordering, std::partial_ordering) based on the nature of the comparisons.
  4. Combine with operator==: Define operator== to ensure complete comparison functionality.

std::span

What Is std::span?

std::span is a lightweight, non-owning reference to a contiguous sequence of objects. Introduced in C++20, it provides a safe and convenient way to pass arrays or parts of containers to functions without losing size information or resorting to raw pointers.

Benefits of Using std::span

  • Safety: Eliminates common pointer-related errors by keeping track of the size.
  • Flexibility: Can reference entire containers or subranges without copying data.
  • Performance: Minimal overhead since it doesn’t own the data.

Creating and Using std::span

Example: Basic Usage of std::span

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

void printSpan(std::span<int> s) {
    for(auto num : s) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::vector<int> vec = {6, 7, 8, 9, 10};

    // Create spans from array and vector
    std::span<int> spanArr(arr);
    std::span<int> spanVec(vec);

    printSpan(spanArr); // Outputs: 1 2 3 4 5 
    printSpan(spanVec); // Outputs: 6 7 8 9 10 

    // Create a subspan
    auto subSpan = spanArr.subspan(1, 3);
    printSpan(subSpan); // Outputs: 2 3 4 

    return 0;
}

Output:

1 2 3 4 5 
6 7 8 9 10 
2 3 4 

Explanation:

  • Creating a std::span: Can be created from arrays, std::vector, or other contiguous containers.
  • Subspan: Allows creating a view into a subset of the span.

Modifying Data Through std::span

Since std::span provides access to the underlying data, modifications are reflected in the original container.

Example: Modifying Data via std::span

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

void incrementElements(std::span<int> s) {
    for(auto& num : s) {
        ++num;
    }
}

int main() {
    std::vector<int> vec = {10, 20, 30, 40, 50};
    std::span<int> spanVec(vec);

    incrementElements(spanVec);

    for(auto num : vec) {
        std::cout << num << " "; // Outputs: 11 21 31 41 51 
    }
    std::cout << std::endl;

    return 0;
}

Output:

11 21 31 41 51 

Explanation:

  • std::span Usage: The incrementElements function takes a span of integers and increments each element.
  • Data Modification: Changes made through the span are directly reflected in the original std::vector.

Constness and std::span

std::span can be used with const types to provide read-only access to data.

Example: Read-Only std::span

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

void printConstSpan(std::span<const int> s) {
    for(auto num : s) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> vec = {100, 200, 300, 400, 500};
    std::span<const int> spanVec(vec);

    printConstSpan(spanVec); // Outputs: 100 200 300 400 500 

    return 0;
}

Output:

100 200 300 400 500 

Explanation:

  • std::span<const int>: Ensures that the elements cannot be modified through the span.

Best Practices with std::span

  1. Prefer std::span Over Raw Pointers: Enhances code safety and clarity by keeping track of size.
  2. Use Const Correctness: Pass spans as std::span<const T> when modification is not required.
  3. Avoid Dangling Spans: Ensure that the underlying data outlives the span to prevent undefined behavior.
  4. Leverage Subspans: Utilize subspan to operate on specific ranges within a span without creating new containers.
  5. Combine with Ranges: Use std::span in conjunction with Ranges for more powerful and expressive data processing.

Other Notable C++20 Features

C++20 introduces several other features and enhancements that further enrich the language. Below are some of the notable additions:

Lambda Enhancements

Template Lambdas

C++20 allows lambda expressions to have template parameters, enabling them to be more generic and flexible.

Example: Template Lambda

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

int main() {
    auto print = []<typename T>(const T& value) {
        std::cout << value << " ";
    };

    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::ranges::for_each(numbers, print); // Outputs: 1 2 3 4 5 

    return 0;
}

Output:

1 2 3 4 5 

Capturing *this by Value

C++20 allows capturing *this by value in lambdas, enabling safer use of member variables.

Example: Capturing *this by Value

#include <iostream>
#include <functional>

struct Counter {
    int count = 0;

    void incrementAfterDelay() {
        auto lambda = [copy = *this]() mutable {
            copy.count++;
            std::cout << "Count after delay: " << copy.count << std::endl;
        };
        // Simulate asynchronous execution
        lambda();
    }
};

int main() {
    Counter c;
    c.incrementAfterDelay(); // Outputs: Count after delay: 1
    std::cout << "Original count: " << c.count << std::endl; // Outputs: Original count: 0
    return 0;
}

Output:

Count after delay: 1
Original count: 0

Explanation:

  • Capturing by Value: The lambda captures a copy of *this, ensuring that modifications within the lambda do not affect the original object.

consteval and constinit

consteval

The consteval keyword specifies that a function is an immediate function, meaning it must be evaluated at compile time.

Example: Using consteval

#include <iostream>

// Immediate function that computes factorial at compile time
consteval int factorial(int n) {
    return (n <= 1) ? 1 : (n * factorial(n - 1));
}

int main() {
    constexpr int fact5 = factorial(5);
    std::cout << "Factorial of 5: " << fact5 << std::endl; // Outputs: 120

    // int fact6 = factorial(6); // Error: factorial must be evaluated at compile time
    return 0;
}

Output:

Factorial of 5: 120

Explanation:

  • consteval Function: Must be evaluated at compile time. Attempting to use it in a runtime context results in a compilation error.

constinit

The constinit keyword ensures that a variable is initialized at compile time, but unlike constexpr, it does not make the variable constant.

Example: Using constinit

#include <iostream>

struct Config {
    int value;
};

// Compile-time initialization
constinit Config config = {42};

int main() {
    std::cout << "Config value: " << config.value << std::endl; // Outputs: 42
    config.value = 100; // Allowed: not const
    std::cout << "Updated Config value: " << config.value << std::endl; // Outputs: 100
    return 0;
}

Output:

Config value: 42
Updated Config value: 100

Explanation:

  • constinit Variable: Must be initialized at compile time but can be modified at runtime.

Template Parameter Lists Enhancements

C++20 introduces new syntax and capabilities for template parameter lists, enhancing their expressiveness.

Template Parameter Lists with auto

Templates can now accept parameters of any type using the auto keyword.

Example: Template with auto Parameters

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

int main() {
    // Template lambda with auto parameters
    auto add = []<typename T>(T a, T b) -> T {
        return a + b;
    };

    std::cout << "Sum: " << add(10, 20) << std::endl;       // Outputs: 30
    std::cout << "Sum: " << add(3.5, 2.5) << std::endl;    // Outputs: 6
    // std::cout << add("Hello", "World") << std::endl;    // Compilation error

    return 0;
}

Output:

Sum: 30
Sum: 6

Explanation:

  • Generic Template Lambda: Uses auto to accept any type that supports the + operator.

std::format

Introduction to std::format

std::format provides a type-safe and extensible way to format strings, similar to Python’s str.format or C#’s string interpolation. It replaces the traditional printf-style formatting with a more modern and secure approach.

Using std::format

Example: Basic Usage of std::format

#include <format>
#include <iostream>

int main() {
    int age = 30;
    std::string name = "Alice";

    std::string message = std::format("Name: {}, Age: {}", name, age);
    std::cout << message << std::endl; // Outputs: Name: Alice, Age: 30

    return 0;
}

Output:

Name: Alice, Age: 30

Explanation:

  • std::format Syntax: Uses {} as placeholders for arguments, providing a clear and readable way to construct formatted strings.

Advanced Formatting

std::format supports various formatting options, including alignment, width, precision, and more.

Example: Advanced Formatting

#include <format>
#include <iostream>

int main() {
    double pi = 3.14159265358979323846;
    std::string hexValue = std::format("{:#x}", 255); // Hexadecimal with prefix

    std::string message = std::format("Pi rounded to 2 decimal places: {:.2f}", pi);
    std::cout << message << std::endl;            // Outputs: Pi rounded to 2 decimal places: 3.14
    std::cout << "Hex value: " << hexValue << std::endl; // Outputs: Hex value: 0xff

    return 0;
}

Output:

Pi rounded to 2 decimal places: 3.14
Hex value: 0xff

Explanation:

  • std::format with Formatting Specifiers: Allows specifying precision and formatting styles directly within the format string.

Enhanced std::chrono

C++20 introduces several enhancements to the <chrono> library, providing more precise and convenient ways to work with time durations and points.

Calendar and Time Zone Support

C++20 adds calendar and time zone support to <chrono>, allowing for more accurate and meaningful time manipulations.

Example: Using std::chrono with Calendars

#include <chrono>
#include <iostream>

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

    // Current system time as a time_point
    auto now = system_clock::now();

    // Convert to time_t for printing
    std::time_t now_time = system_clock::to_time_t(now);
    std::cout << "Current time: " << std::ctime(&now_time) << std::endl;

    // Add 5 days
    auto future = now + days(5);
    std::time_t future_time = system_clock::to_time_t(future);
    std::cout << "Future time (5 days later): " << std::ctime(&future_time) << std::endl;

    return 0;
}

Output:

Current time: Wed Oct 11 14:23:45 2023
Future time (5 days later): Mon Oct 16 14:23:45 2023

Explanation:

  • days Duration: Allows adding days directly to a time_point.
  • Calendar Awareness: Facilitates operations based on calendar dates.

Formatting Dates and Times

With calendar support, formatting dates and times becomes more straightforward.

Example: Formatting Dates with std::format and std::chrono

#include <chrono>
#include <format>
#include <iostream>

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

    // Define a specific date
    year_month_day ymd{year{2023}, month{10}, day{27}};

    // Format the date
    std::string dateStr = std::format("{:%B %d, %Y}", ymd);
    std::cout << "Formatted Date: " << dateStr << std::endl; // Outputs: October 27, 2023

    return 0;
}

Output:

Formatted Date: October 27, 2023

Explanation:

  • year_month_day: Represents a calendar date.
  • std::format with Chrono: Allows formatting using chrono’s calendar types.

Best Practices for Utilizing C++20 Features

  1. Gradual Adoption: Start by integrating C++20 features into new projects or gradually refactor existing codebases.
  2. Leverage Standard Library Enhancements: Utilize new Standard Library features like std::format and enhanced std::chrono for more robust code.
  3. Embrace Modern Syntax: Use Concepts and Ranges to write more expressive and type-safe code.
  4. Optimize with Coroutines: Implement asynchronous operations using coroutines to improve application responsiveness.
  5. Organize Code with Modules: Adopt modules to enhance compile times and code encapsulation, keeping in mind the current limited compiler support.
  6. Stay Updated: Continuously explore and learn about new C++20 features and their best use cases.

Best Practices for Using C++20 Features

Integrating C++20 features into your programming workflow can significantly enhance code quality and performance. However, to maximize their benefits, it’s essential to follow best practices:

  1. Understand the Features Thoroughly: Before using a new feature, ensure you understand its mechanics, benefits, and potential pitfalls.
  2. Start Small: Begin by applying C++20 features to smaller, non-critical parts of your codebase to gain confidence.
  3. Maintain Consistency: Use C++20 features consistently across your projects to ensure uniformity and ease of maintenance.
  4. Leverage Compiler Support: Ensure that your compiler fully supports the C++20 features you intend to use and stay updated with the latest compiler versions.
  5. Write Clear and Readable Code: While advanced features offer powerful abstractions, prioritize code readability and maintainability.
  6. Use Static Analysis Tools: Employ tools that can analyze and enforce best practices, ensuring that C++20 features are used correctly.
  7. Stay Informed: Keep abreast of updates, community discussions, and evolving best practices related to C++20.

Practical Examples

To solidify your understanding of C++20 features, let’s explore some practical examples that demonstrate how these features can be applied in real-world scenarios.

Example 1: Using Concepts to Create a Generic Function

Problem: Create a generic multiply function that only works with arithmetic types.

Solution:

#include <concepts>
#include <iostream>

// Define the 'Arithmetic' concept
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;

// Generic multiply function constrained by the 'Arithmetic' concept
Arithmetic auto multiply(Arithmetic auto a, Arithmetic auto b) {
    return a * b;
}

int main() {
    std::cout << multiply(3, 4) << std::endl;       // Outputs: 12
    std::cout << multiply(2.5, 4.2) << std::endl;   // Outputs: 10.5
    // std::cout << multiply("Hello", "World") << std::endl; // Compilation error
    return 0;
}

Output:

12
10.5

Explanation:

  • The multiply function is constrained to accept only types that satisfy the Arithmetic concept, ensuring type safety.

Example 2: Implementing a Coroutine-Based Generator

Problem: Create a generator that produces an infinite sequence of Fibonacci numbers using coroutines.

Solution:

#include <coroutine>
#include <iostream>
#include <optional>

// Generator class for Fibonacci numbers
struct FibonacciGenerator {
    struct promise_type {
        std::optional<long long> current_value;

        FibonacciGenerator get_return_object() {
            return FibonacciGenerator{ 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(long long value) {
            current_value = value;
            return {};
        }

        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle<promise_type> handle;

    FibonacciGenerator(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~FibonacciGenerator() { if(handle) handle.destroy(); }

    std::optional<long long> next() {
        if(!handle.done()) {
            handle.resume();
            return handle.promise().current_value;
        }
        return std::nullopt;
    }
};

// Coroutine function to generate Fibonacci numbers
FibonacciGenerator generateFibonacci() {
    long long a = 0, b = 1;
    while(true) {
        co_yield a;
        auto temp = a;
        a = b;
        b = temp + b;
    }
}

int main() {
    auto fibGen = generateFibonacci();
    for(int i = 0; i < 10; ++i) {
        if(auto val = fibGen.next()) {
            std::cout << *val << " "; // Outputs: 0 1 1 2 3 5 8 13 21 34 
        }
    }
    std::cout << std::endl;
    return 0;
}

Output:

0 1 1 2 3 5 8 13 21 34 

Explanation:

  • Infinite Coroutine: The generateFibonacci coroutine runs indefinitely, yielding Fibonacci numbers.
  • Generator Usage: The FibonacciGenerator class manages the coroutine, providing a next method to retrieve values.

Example 3: Using std::span to Manipulate Subarrays

Problem: Implement a function that reverses a subarray within a given array using std::span.

Solution:

#include <algorithm>
#include <iostream>
#include <span>
#include <array>

// Function to reverse a subarray using std::span
void reverseSubarray(std::span<int> s, size_t start, size_t length) {
    if(start + length > s.size()) {
        throw std::out_of_range("Subarray exceeds span bounds.");
    }
    std::span<int> sub = s.subspan(start, length);
    std::reverse(sub.begin(), sub.end());
}

int main() {
    std::array<int, 8> data = {1, 2, 3, 4, 5, 6, 7, 8};
    
    try {
        // Reverse elements from index 2 to index 5 (elements 3,4,5,6)
        reverseSubarray(data, 2, 4);
        
        for(auto num : data) {
            std::cout << num << " "; // Outputs: 1 2 6 5 4 3 7 8 
        }
        std::cout << std::endl;
    } catch(const std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
    
    return 0;
}

Output:

1 2 6 5 4 3 7 8 

Explanation:

  • std::span Usage: The reverseSubarray function takes a span of integers and reverses a specified subrange.
  • Safety: Ensures that the subarray does not exceed the bounds of the span, throwing an exception if it does.

Example 4: Implementing Defaulted Comparisons with the <=> Operator

Problem: Create a Rectangle class that can be compared using all relational and equality operators without manually defining each operator.

Solution:

#include <compare>
#include <iostream>

struct Rectangle {
    int width;
    int height;

    // Default the three-way comparison operator
    auto operator<=>(const Rectangle&) const = default;
};

int main() {
    Rectangle r1{10, 20};
    Rectangle r2{10, 20};
    Rectangle r3{15, 25};

    std::cout << std::boolalpha;
    std::cout << "r1 == r2: " << (r1 == r2) << std::endl; // true
    std::cout << "r1 < r3: " << (r1 < r3) << std::endl;   // true
    std::cout << "r3 > r2: " << (r3 > r2) << std::endl;   // true

    return 0;
}

Output:

r1 == r2: true
r1 < r3: true
r3 > r2: true

Explanation:

  • Defaulted <=> Operator: Automatically generates comparison logic based on member variables.
  • Comprehensive Comparisons: Enables the use of ==, <, >, <=, >=, and != without additional code.

Summary

C++20 represents a significant advancement in the C++ language, introducing features that enhance its expressiveness, performance, and usability. In this chapter, you’ve explored some of the most impactful C++20 features:

  • Concepts: Enabled more expressive and type-safe template programming.
  • Ranges: Provided a modern and flexible approach to working with sequences of elements.
  • Coroutines: Simplified asynchronous programming by allowing functions to suspend and resume execution.
  • Modules: Improved code organization, encapsulation, and compile times by introducing a new way to manage dependencies, though with currently limited support across major compilers.
  • Three-Way Comparison Operator (<=>): Streamlined the implementation of comparison operators, reducing boilerplate code.
  • std::span: Offered a safe and efficient way to reference contiguous sequences of elements without owning them.
  • Other Enhancements: Including lambda improvements, consteval and constinit, template parameter list enhancements, std::format, and an enriched std::chrono library.

By integrating these features into your C++ programming practices, you can write more efficient, readable, and maintainable code. Embracing C++20’s modern features not only keeps your skills up-to-date but also leverages the full potential of the C++ language for developing robust and high-performance applications.


Now you’re ready to move on to Filesystem, where you’ll explore C++20’s capabilities for handling file systems, enabling you to build applications that interact seamlessly with the environment.

Next chapter: Filesystem