- •Contents
- •Preface
- •An Experiment
- •Acknowledgments
- •Versions of this Book
- •About This Book
- •What You Should Know Before Reading This Book
- •Overall Structure of the Book
- •The Way I Implement
- •Initializations
- •Error Terminology
- •The C++ Standards
- •Example Code and Additional Information
- •1 Comparisons and Operator <=>
- •1.1.1 Defining Comparison Operators Before C++20
- •1.1.2 Defining Comparison Operators Since C++20
- •1.2 Defining and Using Comparisons
- •1.2.2 Comparison Category Types
- •1.2.4 Calling Operator <=> Directly
- •1.2.5 Dealing with Multiple Ordering Criteria
- •1.3 Defining operator<=> and operator==
- •1.3.1 Defaulted operator== and operator<=>
- •1.3.2 Defaulted operator<=> Implies Defaulted operator==
- •1.3.3 Implementation of the Defaulted operator<=>
- •1.4 Overload Resolution with Rewritten Expressions
- •1.5 Using Operator <=> in Generic Code
- •1.5.1 compare_three_way
- •1.6 Compatibility Issues with the Comparison Operators
- •1.6.2 Inheritance with Protected Members
- •1.7 Afternotes
- •2 Placeholder Types for Function Parameters
- •2.1.1 auto for Parameters of Member Functions
- •2.2 Using auto for Parameters in Practice
- •2.3.1 Basic Constraints for auto Parameters
- •2.4 Afternotes
- •3.1 Motivating Example of Concepts and Requirements
- •3.1.1 Improving the Template Step by Step
- •3.1.2 A Complete Example with Concepts
- •3.2 Where Constraints and Concepts Can Be Used
- •3.2.1 Constraining Alias Templates
- •3.2.2 Constraining Variable Templates
- •3.2.3 Constraining Member Functions
- •3.3 Typical Applications of Concepts and Constraints in Practice
- •3.3.1 Using Concepts to Understand Code and Error Messages
- •3.3.2 Using Concepts to Disable Generic Code
- •3.3.3 Using Requirements to Call Different Functions
- •3.3.4 The Example as a Whole
- •3.3.5 Former Workarounds
- •3.4 Semantic Constraints
- •3.4.1 Examples of Semantic Constraints
- •3.5 Design Guidelines for Concepts
- •3.5.1 Concepts Should Group Requirements
- •3.5.2 Define Concepts with Care
- •3.5.3 Concepts versus Type Traits and Boolean Expressions
- •3.6 Afternotes
- •4.1 Constraints
- •4.2.1 Using && and || in requires Clauses
- •4.3 Ad-hoc Boolean Expressions
- •4.4.1 Simple Requirements
- •4.4.2 Type Requirements
- •4.4.3 Compound Requirements
- •4.4.4 Nested Requirements
- •4.5 Concepts in Detail
- •4.5.1 Defining Concepts
- •4.5.2 Special Abilities of Concepts
- •4.5.3 Concepts for Non-Type Template Parameters
- •4.6 Using Concepts as Type Constraints
- •4.7 Subsuming Constraints with Concepts
- •4.7.1 Indirect Subsumptions
- •4.7.2 Defining Commutative Concepts
- •5 Standard Concepts in Detail
- •5.1 Overview of All Standard Concepts
- •5.1.1 Header Files and Namespaces
- •5.1.2 Standard Concepts Subsume
- •5.2 Language-Related Concepts
- •5.2.1 Arithmetic Concepts
- •5.2.2 Object Concepts
- •5.2.3 Concepts for Relationships between Types
- •5.2.4 Comparison Concepts
- •5.3 Concepts for Iterators and Ranges
- •5.3.1 Concepts for Ranges and Views
- •5.3.2 Concepts for Pointer-Like Objects
- •5.3.3 Concepts for Iterators
- •5.3.4 Iterator Concepts for Algorithms
- •5.4 Concepts for Callables
- •5.4.1 Basic Concepts for Callables
- •5.4.2 Concepts for Callables Used by Iterators
- •5.5 Auxiliary Concepts
- •5.5.1 Concepts for Specific Type Attributes
- •5.5.2 Concepts for Incrementable Types
- •6 Ranges and Views
- •6.1 A Tour of Ranges and Views Using Examples
- •6.1.1 Passing Containers to Algorithms as Ranges
- •6.1.2 Constraints and Utilities for Ranges
- •6.1.3 Views
- •6.1.4 Sentinels
- •6.1.5 Range Definitions with Sentinels and Counts
- •6.1.6 Projections
- •6.1.7 Utilities for Implementing Code for Ranges
- •6.1.8 Limitations and Drawbacks of Ranges
- •6.2 Borrowed Iterators and Ranges
- •6.2.1 Borrowed Iterators
- •6.2.2 Borrowed Ranges
- •6.3 Using Views
- •6.3.1 Views on Ranges
- •6.3.2 Lazy Evaluation
- •6.3.3 Caching in Views
- •6.3.4 Performance Issues with Filters
- •6.4 Views on Ranges That Are Destroyed or Modified
- •6.4.1 Lifetime Dependencies Between Views and Their Ranges
- •6.4.2 Views with Write Access
- •6.4.3 Views on Ranges That Change
- •6.4.4 Copying Views Might Change Behavior
- •6.5 Views and const
- •6.5.1 Generic Code for Both Containers and Views
- •6.5.3 Bringing Back Deep Constness to Views
- •6.6 Summary of All Container Idioms Broken By Views
- •6.7 Afternotes
- •7 Utilities for Ranges and Views
- •7.1 Key Utilities for Using Ranges as Views
- •7.1.1 std::views::all()
- •7.1.2 std::views::counted()
- •7.1.3 std::views::common()
- •7.2 New Iterator Categories
- •7.3 New Iterator and Sentinel Types
- •7.3.1 std::counted_iterator
- •7.3.2 std::common_iterator
- •7.3.3 std::default_sentinel
- •7.3.4 std::unreachable_sentinel
- •7.3.5 std::move_sentinel
- •7.4 New Functions for Dealing with Ranges
- •7.4.1 Functions for Dealing with the Elements of Ranges (and Arrays)
- •7.4.2 Functions for Dealing with Iterators
- •7.4.3 Functions for Swapping and Moving Elements/Values
- •7.4.4 Functions for Comparisons of Values
- •7.5 New Type Functions/Utilities for Dealing with Ranges
- •7.5.1 Generic Types of Ranges
- •7.5.2 Generic Types of Iterators
- •7.5.3 New Functional Types
- •7.5.4 Other New Types for Dealing with Iterators
- •7.6 Range Algorithms
- •7.6.1 Benefits and Restrictions for Range Algorithms
- •7.6.2 Algorithm Overview
- •8 View Types in Detail
- •8.1 Overview of All Views
- •8.1.1 Overview of Wrapping and Generating Views
- •8.1.2 Overview of Adapting Views
- •8.2 Base Class and Namespace of Views
- •8.2.1 Base Class for Views
- •8.2.2 Why Range Adaptors/Factories Have Their Own Namespace
- •8.3 Source Views to External Elements
- •8.3.1 Subrange
- •8.3.2 Ref View
- •8.3.3 Owning View
- •8.3.4 Common View
- •8.4 Generating Views
- •8.4.1 Iota View
- •8.4.2 Single View
- •8.4.3 Empty View
- •8.4.4 IStream View
- •8.4.5 String View
- •8.4.6 Span
- •8.5 Filtering Views
- •8.5.1 Take View
- •8.5.2 Take-While View
- •8.5.3 Drop View
- •8.5.4 Drop-While View
- •8.5.5 Filter View
- •8.6 Transforming Views
- •8.6.1 Transform View
- •8.6.2 Elements View
- •8.6.3 Keys and Values View
- •8.7 Mutating Views
- •8.7.1 Reverse View
- •8.8 Views for Multiple Ranges
- •8.8.1 Split and Lazy-Split View
- •8.8.2 Join View
- •9 Spans
- •9.1 Using Spans
- •9.1.1 Fixed and Dynamic Extent
- •9.1.2 Example Using a Span with a Dynamic Extent
- •9.1.4 Example Using a Span with Fixed Extent
- •9.1.5 Fixed vs. Dynamic Extent
- •9.2 Spans Considered Harmful
- •9.3 Design Aspects of Spans
- •9.3.1 Lifetime Dependencies of Spans
- •9.3.2 Performance of Spans
- •9.3.3 const Correctness of Spans
- •9.3.4 Using Spans as Parameters in Generic Code
- •9.4 Span Operations
- •9.4.1 Span Operations and Member Types Overview
- •9.4.2 Constructors
- •9.5 Afternotes
- •10 Formatted Output
- •10.1 Formatted Output by Example
- •10.1.1 Using std::format()
- •10.1.2 Using std::format_to_n()
- •10.1.3 Using std::format_to()
- •10.1.4 Using std::formatted_size()
- •10.2 Performance of the Formatting Library
- •10.2.1 Using std::vformat() and vformat_to()
- •10.3 Formatted Output in Detail
- •10.3.1 General Format of Format Strings
- •10.3.2 Standard Format Specifiers
- •10.3.3 Width, Precision, and Fill Characters
- •10.3.4 Format/Type Specifiers
- •10.4 Internationalization
- •10.5 Error Handling
- •10.6 User-Defined Formatted Output
- •10.6.1 Basic Formatter API
- •10.6.2 Improved Parsing
- •10.6.3 Using Standard Formatters for User-Defined Formatters
- •10.6.4 Using Standard Formatters for Strings
- •10.7 Afternotes
- •11 Dates and Timezones for <chrono>
- •11.1 Overview by Example
- •11.1.1 Scheduling a Meeting on the 5th of Every Month
- •11.1.2 Scheduling a Meeting on the Last Day of Every Month
- •11.1.3 Scheduling a Meeting Every First Monday
- •11.1.4 Using Different Timezones
- •11.2 Basic Chrono Concepts and Terminology
- •11.3 Basic Chrono Extensions with C++20
- •11.3.1 Duration Types
- •11.3.2 Clocks
- •11.3.3 Timepoint Types
- •11.3.4 Calendrical Types
- •11.3.5 Time Type hh_mm_ss
- •11.3.6 Hours Utilities
- •11.4 I/O with Chrono Types
- •11.4.1 Default Output Formats
- •11.4.2 Formatted Output
- •11.4.3 Locale-Dependent Output
- •11.4.4 Formatted Input
- •11.5 Using the Chrono Extensions in Practice
- •11.5.1 Invalid Dates
- •11.5.2 Dealing with months and years
- •11.5.3 Parsing Timepoints and Durations
- •11.6 Timezones
- •11.6.1 Characteristics of Timezones
- •11.6.2 The IANA Timezone Database
- •11.6.3 Using Timezones
- •11.6.4 Dealing with Timezone Abbreviations
- •11.6.5 Custom Timezones
- •11.7 Clocks in Detail
- •11.7.1 Clocks with a Specified Epoch
- •11.7.2 The Pseudo Clock local_t
- •11.7.3 Dealing with Leap Seconds
- •11.7.4 Conversions between Clocks
- •11.7.5 Dealing with the File Clock
- •11.8 Other New Chrono Features
- •11.9 Afternotes
- •12 std::jthread and Stop Tokens
- •12.1 Motivation for std::jthread
- •12.1.1 The Problem of std::thread
- •12.1.2 Using std::jthread
- •12.1.3 Stop Tokens and Stop Callbacks
- •12.1.4 Stop Tokens and Condition Variables
- •12.2 Stop Sources and Stop Tokens
- •12.2.1 Stop Sources and Stop Tokens in Detail
- •12.2.2 Using Stop Callbacks
- •12.2.3 Constraints and Guarantees of Stop Tokens
- •12.3.1 Using Stop Tokens with std::jthread
- •12.4 Afternotes
- •13 Concurrency Features
- •13.1 Thread Synchronization with Latches and Barriers
- •13.1.1 Latches
- •13.1.2 Barriers
- •13.2 Semaphores
- •13.2.1 Example of Using Counting Semaphores
- •13.2.2 Example of Using Binary Semaphores
- •13.3 Extensions for Atomic Types
- •13.3.1 Atomic References with std::atomic_ref<>
- •13.3.2 Atomic Shared Pointers
- •13.3.3 Atomic Floating-Point Types
- •13.3.4 Thread Synchronization with Atomic Types
- •13.3.5 Extensions for std::atomic_flag
- •13.4 Synchronized Output Streams
- •13.4.1 Motivation for Synchronized Output Streams
- •13.4.2 Using Synchronized Output Streams
- •13.4.3 Using Synchronized Output Streams for Files
- •13.4.4 Using Synchronized Output Streams as Output Streams
- •13.4.5 Synchronized Output Streams in Practice
- •13.5 Afternotes
- •14 Coroutines
- •14.1 What Are Coroutines?
- •14.2 A First Coroutine Example
- •14.2.1 Defining the Coroutine
- •14.2.2 Using the Coroutine
- •14.2.3 Lifetime Issues with Call-by-Reference
- •14.2.4 Coroutines Calling Coroutines
- •14.2.5 Implementing the Coroutine Interface
- •14.2.6 Bootstrapping Interface, Handle, and Promise
- •14.2.7 Memory Management
- •14.3 Coroutines That Yield or Return Values
- •14.3.1 Using co_yield
- •14.3.2 Using co_return
- •14.4 Coroutine Awaitables and Awaiters
- •14.4.1 Awaiters
- •14.4.2 Standard Awaiters
- •14.4.3 Resuming Sub-Coroutines
- •14.4.4 Passing Values From Suspension Back to the Coroutine
- •14.5 Afternotes
- •15 Coroutines in Detail
- •15.1 Coroutine Constraints
- •15.1.1 Coroutine Lambdas
- •15.2 The Coroutine Frame and the Promises
- •15.2.1 How Coroutine Interfaces, Promises, and Awaitables Interact
- •15.3 Coroutine Promises in Detail
- •15.3.1 Mandatory Promise Operations
- •15.3.2 Promise Operations to Return or Yield Values
- •15.3.3 Optional Promise Operations
- •15.4 Coroutine Handles in Detail
- •15.4.1 std::coroutine_handle<void>
- •15.5 Exceptions in Coroutines
- •15.6 Allocating Memory for the Coroutine Frame
- •15.6.1 How Coroutines Allocate Memory
- •15.6.2 Avoiding Heap Memory Allocation
- •15.6.3 get_return_object_on_allocation_failure()
- •15.7 co_await and Awaiters in Detail
- •15.7.1 Details of the Awaiter Interface
- •15.7.2 Letting co_await Update Running Coroutines
- •15.7.3 Symmetric Transfer with Awaiters for Continuation
- •15.8.1 await_transform()
- •15.8.2 operator co_await()
- •15.9 Concurrent Use of Coroutines
- •15.9.1 co_await Coroutines
- •15.9.2 A Thread Pool for Coroutine Tasks
- •15.9.3 What C++ Libraries Will Provide After C++20
- •15.10 Coroutine Traits
- •16 Modules
- •16.1 Motivation for Modules Using a First Example
- •16.1.1 Implementing and Exporting a Module
- •16.1.2 Compiling Module Units
- •16.1.3 Importing and Using a Module
- •16.1.4 Reachable versus Visible
- •16.1.5 Modules and Namespaces
- •16.2 Modules with Multiple Files
- •16.2.1 Module Units
- •16.2.2 Using Implementation Units
- •16.2.3 Internal Partitions
- •16.2.4 Interface Partitions
- •16.2.5 Summary of Splitting Modules into Different Files
- •16.3 Dealing with Modules in Practice
- •16.3.1 Dealing with Module Files with Different Compilers
- •16.3.2 Dealing with Header Files
- •16.4 Modules in Detail
- •16.4.1 Private Module Fragments
- •16.4.2 Module Declaration and Export in Detail
- •16.4.3 Umbrella Modules
- •16.4.4 Module Import in Detail
- •16.4.5 Reachable versus Visible Symbols in Detail
- •16.5 Afternotes
- •17 Lambda Extensions
- •17.1 Generic Lambdas with Template Parameters
- •17.1.1 Using Template Parameters for Generic Lambdas in Practice
- •17.1.2 Explicit Specification of Lambda Template Parameters
- •17.2 Calling the Default Constructor of Lambdas
- •17.4 consteval Lambdas
- •17.5 Changes for Capturing
- •17.5.1 Capturing this and *this
- •17.5.2 Capturing Structured Bindings
- •17.5.3 Capturing Parameter Packs of Variadic Templates
- •17.5.4 Lambdas as Coroutines
- •17.6 Afternotes
- •18 Compile-Time Computing
- •18.1 Keyword constinit
- •18.1.1 Using constinit in Practice
- •18.1.2 How constinit Solves the Static Initialization Order Fiasco
- •18.2.1 A First consteval Example
- •18.2.2 constexpr versus consteval
- •18.2.3 Using consteval in Practice
- •18.2.4 Compile-Time Value versus Compile-Time Context
- •18.4 std::is_constant_evaluated()
- •18.4.1 std::is_constant_evaluated() in Detail
- •18.5 Using Heap Memory, Vectors, and Strings at Compile Time
- •18.5.1 Using Vectors at Compile Time
- •18.5.2 Returning a Collection at Compile Time
- •18.5.3 Using Strings at Compile Time
- •18.6.1 constexpr Language Extensions
- •18.6.2 constexpr Library Extensions
- •18.7 Afternotes
- •19.1 New Types for Non-Type Template Parameters
- •19.1.1 Floating-Point Values as Non-Type Template Parameters
- •19.1.2 Objects as Non-Type Template Parameters
- •19.2 Afternotes
- •20 New Type Traits
- •20.1 New Type Traits for Type Classification
- •20.1.1 is_bounded_array_v<> and is_unbounded_array_v
- •20.2 New Type Traits for Type Inspection
- •20.2.1 is_nothrow_convertible_v<>
- •20.3 New Type Traits for Type Conversion
- •20.3.1 remove_cvref_t<>
- •20.3.2 unwrap_reference<> and unwrap_ref_decay_t
- •20.3.3 common_reference<>_t
- •20.3.4 type_identity_t<>
- •20.4 New Type Traits for Iterators
- •20.4.1 iter_difference_t<>
- •20.4.2 iter_value_t<>
- •20.4.3 iter_reference_t<> and iter_rvalue_reference_t<>
- •20.5 Type Traits and Functions for Layout Compatibility
- •20.5.1 is_layout_compatible_v<>
- •20.5.2 is_pointer_interconvertible_base_of_v<>
- •20.5.3 is_corresponding_member()
- •20.5.4 is_pointer_interconvertible_with_class()
- •20.6 Afternotes
- •21 Small Improvements for the Core Language
- •21.1 Range-Based for Loop with Initialization
- •21.2 using for Enumeration Values
- •21.3 Delegating Enumeration Types to Different Scopes
- •21.4 New Character Type char8_t
- •21.4.2 Broken Backward Compatibility
- •21.5 Improvements for Aggregates
- •21.5.1 Designated Initializers
- •21.5.2 Aggregate Initialization with Parentheses
- •21.5.3 Definition of Aggregates
- •21.6 New Attributes and Attribute Features
- •21.6.1 Attributes [[likely]] and [[unlikely]]
- •21.6.2 Attribute [[no_unique_address]]
- •21.6.3 Attribute [[nodiscard]] with Parameter
- •21.7 Feature Test Macros
- •21.8 Afternotes
- •22 Small Improvements for Generic Programming
- •22.1 Implicit typename for Type Members of Template Parameters
- •22.1.1 Rules for Implicit typename in Detail
- •22.2 Improvements for Aggregates in Generic Code
- •22.2.1 Class Template Argument Deduction (CTAD) for Aggregates
- •22.3.1 Conditional explicit in the Standard Library
- •22.4 Afternotes
- •23 Small Improvements for the C++ Standard Library
- •23.1 Updates for String Types
- •23.1.1 String Members starts_with() and ends_with()
- •23.1.2 Restricted String Member reserve()
- •23.2 std::source_location
- •23.3 Safe Comparisons of Integral Values and Sizes
- •23.3.1 Safe Comparisons of Integral Values
- •23.3.2 ssize()
- •23.4 Mathematical Constants
- •23.5 Utilities for Dealing with Bits
- •23.5.1 Bit Operations
- •23.5.2 std::bit_cast<>()
- •23.5.3 std::endian
- •23.6 <version>
- •23.7 Extensions for Algorithms
- •23.7.1 Range Support
- •23.7.2 New Algorithms
- •23.7.3 unseq Execution Policy for Algorithms
- •23.8 Afternotes
- •24 Deprecated and Removed Features
- •24.1 Deprecated and Removed Core Language Features
- •24.2 Deprecated and Removed Library Features
- •24.2.1 Deprecated Library Features
- •24.2.2 Removed Library Features
- •24.3 Afternotes
- •Glossary
- •Index
14.2 A First Coroutine Example |
471 |
After callCoro() initializes coro(), coro() is resumed only once, which means that only the first part of it is called. After that, the suspension of coro() transfers the control flow back to callCoro(), which then suspends itself, meaning that the control flow goes back to main(). When main() resumes callCoro(), the program finishes callCoro(). coro() is never finished at all.
Delegating resume()
It is possible to implement CoroTask so that co_await registers a sub-coroutine in a way that a suspension in the sub-coroutine is handled like a suspension in the calling coroutine. callCoro() would then look as follows:
CoroTaskSub callCoro()
{
std::cout << " |
callCoro(): CALL coro()\n"; |
|
co_await coro(); |
|
// call sub-coroutine |
std::cout << " |
callCoro(): coro() done\n"; |
|
co_await std::suspend_always{}; |
// SUSPEND |
|
std::cout << " |
callCoro(): END\n"; |
|
}
However, resume() then has to delegate a request for resumption to the sub-coroutine if there is one. For this purpose, the CoroTask interface has to become an awaitable. Later, after we have introduced awaitables, we will see such a coroutine interface that delegates resumptions to sub-coroutines.
14.2.5 Implementing the Coroutine Interface
I have stated a couple of times that in our example, class CoroTask plays an important role for dealing with the coroutine. It is the interface that both the compiler and the caller of a coroutine usually deal with. The coroutine interface brings together a couple of requirements to let the compiler deal with coroutines and provides the API for the caller to create, resume, and destroy the coroutine. Let us elaborate on that in detail.
To deal with coroutines in C++ we need two things:
• A promise type
This type is used to define certain customization points for dealing with a coroutine. Specific member functions define callbacks that are called in certain situations.
• An internal coroutine handle of the type std::coroutine_handle<>
This object is created when a coroutine is called (using one of the standard callbacks of the promise type above). It can be used to manage the state of the coroutine by providing a low-level interface to resume a coroutine as well as to deal with the end of the coroutine.
The usual purpose of the type dealing with the return type of a coroutine is to bring these requirements together:
•It has to define which promise type is used (it is usually defined as the type member promise_type).
•It has to define where the coroutine handle is stored (it is usually defined as a data member).
•It has to provide the interface for the caller to deal with the coroutine (the member function resume() in this case).
472 |
Chapter 14: Coroutines |
The Coroutine Interface CoroTask
The class CoroTask, which provides promise_type, stores the coroutine handle, and defines the API for the caller of the coroutine, may be defined as follows:
coro/corotask.hpp
#include <coroutine>
//coroutine interface to deal with a simple task
//- providing resume() to resume the coroutine
class [[nodiscard]] CoroTask { public:
// initialize members for state and customization:
struct promise_type; |
// definition later in corotaskpromise.hpp |
using CoroHdl = std::coroutine_handle<promise_type>; |
|
private: |
|
CoroHdl hdl; |
// native coroutine handle |
public: |
|
// constructor and destructor: |
|
CoroTask(auto h) |
|
: hdl{h} { |
// store coroutine handle in interface |
} |
|
~CoroTask() { |
|
if (hdl) { |
|
hdl.destroy(); |
// destroy coroutine handle |
} |
|
} |
|
// don’t copy or move:
CoroTask(const CoroTask&) = delete;
CoroTask& operator=(const CoroTask&) = delete;
//API to resume the coroutine
//- returns whether there is still something to process
bool resume() const {
if (!hdl || hdl.done()) {
return false; |
// nothing (more) to process |
} |
|
hdl.resume(); |
// RESUME (blocks until suspended again or the end) |
return !hdl.done(); |
|
} |
|
}; |
|
#include "corotaskpromise.hpp" // definition of promise_type |
14.2 A First Coroutine Example |
473 |
In the class CoroTask, we first define the basic types and member to deal with the native API of the coroutine. The key member the compiler looks for in a coroutine interface type is the type member promise_type. With the promise type, we define the type of the coroutine handle and introduce a private member where this coroutine handle, specialized for the promise type, is stored:
class [[nodiscard]] CoroTask { public:
// initialize members for state and customization:
struct promise_type; |
// definition later in corotaskpromise.hpp |
using CoroHdl = std::coroutine_handle<promise_type>; |
|
private: |
|
CoroHdl hdl; |
// native coroutine handle |
... |
|
};
We introduce the promise_type (which every coroutine type has to have) and declare the native coroutine handle hdl, which manages the state of the coroutine. As you can see, the type of the native coroutine handle, std::coroutine_handle<>, is parameterized with the promise type. That way, any data stored in the promise is part of the handle and the functions in the promise are available via the handle.
The promise type has to be public to be visible from the outside. It is often also helpful to provide a public name of the type of the coroutine handle (CoroHdl in this case). To simplify things, we could even make the handle itself public. That way, we can use
CoroTask::CoroHdl
instead of
std::coroutine_handle<CoroTask::promise_type>
We could also define the promise type here directly inline. However, in this example, we defer the definition to the later included corotaskpromise.hpp.
The constructor and destructor of the coroutine interface type initialize the member for the coroutine handle and clean it up before the coroutine interface is destroyed:
class CoroTask { |
|
... |
|
public: |
|
CoroTask(auto h) |
|
: hdl{h} { |
// store coroutine handle internally |
} |
|
~CoroTask() { |
|
if (hdl) { |
|
hdl.destroy(); |
// destroy coroutine handle (if there is one) |
} |
|
} |
|
... |
|
}; |
|
474 Chapter 14: Coroutines
By declaring the class with [[nodiscard]], we force compiler warnings when the coroutine is created but not used (which might especially happen by accidentally using a coroutine as an ordinary function).
For simplicity, we disable copying and moving. Providing copy or move semantics is possible but you have to be careful to deal with the resources correctly.
Finally, we define our only interface for the caller, resume():
class CoroTask {
...
bool resume() const {
if (!hdl || hdl.done()) {
return false; |
// nothing (more) to process |
} |
|
hdl.resume(); |
// RESUME (blocks until suspended again or the end) |
return !hdl.done(); |
|
} |
|
}; |
|
The key API is resume(), which resumes the coroutine when it is suspended. It more or less propagates the request to resume to the native coroutine handle. It also returns whether it makes sense to resume the coroutine again.
First, the function checks whether we have a handle at all or whether the coroutine has already ended. Even though in this implementation the coroutine interface always has a handle, this is a safety net which is necessary, for example, if the interface supports move semantics.
Calling resume() is only allowed when the coroutine is suspended and has not reached its end. Therefore, checking whether it is done() is necessary. The call itself resumes the suspended coroutine and blocks until it reaches the next suspend point or the end.
hdl.resume(); |
// RESUME (blocks until suspended again or the end) |
You can also use operator() for this: |
|
hdl(); |
// RESUME (blocks until suspended again or the end) |
Because our resume() interface for the caller returns whether it makes sense to resume the coroutine again, we return whether the coroutine has reached its end:
bool resume() const {
...
return !hdl.done();
}
The member function done() is provided by the native coroutine handle just for that purpose.
As you can see, the interface for the caller of the coroutine wraps the native coroutine handle and its API completely. In this API, we decide how the caller can deal with the coroutine. We could use different function names or even operators or split the calls for resumption and checking for the end. Later on you will see examples where we iterate over values the coroutine delivers with each suspension, we can even send values back to the coroutine, and we can provide an API to put the coroutine in a different context.
14.2 A First Coroutine Example |
475 |
Finally, we include a header file with the definition of the promise type:
#include "corotaskpromise.hpp" // definition of promise_type
This is usually done inside the declaration of the interface class or at least in the same header file. However, this way, we can split the details of this example over different files.
Implementing promise_type
The only missing piece is the definition of the promise type. Its purpose is to:
•Define how to create or get the return value of the coroutine (which usually includes creating the coroutine handle)
•Decide whether the coroutine should suspend at its beginning or end
•Deal with values exchanged between the caller of the coroutine and the coroutine
•Deal with unhandled exceptions
Here is a typical basic implementation of the promise type for CoroTask and its coroutine handle type
CoroHdl: |
|
|||
|
||||
coro/corotaskpromise.hpp |
|
|
|
|
|
struct CoroTask::promise_type { |
|
|
|
|
auto get_return_object() { |
// init and return the coroutine interface |
|
|
|
return CoroTask{CoroHdl::from_promise(*this)}; |
|
||
|
} |
|
|
|
|
auto initial_suspend() { |
// initial suspend point |
|
|
|
return std::suspend_always{}; |
// - suspend immediately |
|
|
|
} |
|
|
|
|
void unhandled_exception() { |
// deal with exceptions |
|
|
|
std::terminate(); |
// - terminate the program |
|
|
|
} |
|
|
|
|
void return_void() { |
// deal with the end or co_return; |
|
|
|
} |
|
|
|
|
auto final_suspend() noexcept { |
// final suspend point |
|
|
|
return std::suspend_always{}; |
// - suspend immediately |
|
|
|
} |
|
|
|
|
|
|
||
|
}; |
|
|
|
We (have to) define the following members (using the typical order in which they are used):
•get_return_object() is called to initialize the coroutine interface. It creates the object that is later returned to the caller of the coroutine. It is usually implemented as follows:
– First, it creates the native coroutine handle for the promise that we call this function for:
coroHdl = CoroHdl::from_promise(*this)
The promise that we call this member function for is automatically created when we start the coroutine. from_promise() is a static member function that the class template std::coroutine_handle<>
provides for this purpose.
476 |
Chapter 14: Coroutines |
–Then, we create the coroutine interface object, initializing it with the handle just created: coroIf = CoroTask{coroHdl}
–Finally, we return the interface object:
return coroIf
The implementation does this all in one statement:
auto get_return_object() {
return CoroTask{CoroHdl::from_promise(*this)};
}
We could also return the coroutine handle here without explicitly creating the coroutine interface from it:
auto get_return_object() {
return CoroHdl::from_promise(*this);
}
Internally, the returned coroutine handle is then automatically converted by using it to initialize the coroutine interface. However, this approach is not recommended, because it does not work if the constructor of CoroTask is explicit and because it is not clear when the interface is created.
•initial_suspend() allows additional initial preparations and defines whether the coroutine should start eagerly or lazily:
–Returning std::suspend_never{} means starting eagerly. The coroutine starts immediately after its initialization with the first statements.
–Returning std::suspend_always{} means starting lazily. The coroutine is suspended immediately without executing any statement. It is processed with the resumption.
In our example, we request immediate suspension.
•return_void() defines the reaction when reaching the end (or a co_return; statement). it this member function is declared, the coroutine should never return a value. If the coroutine yields or returns data, you have to use another member function instead.
•unhandled_exception() defines how to deal with exceptions not handled locally in the coroutine. Here, we specify that this results in an abnormal termination of the program. Other ways of dealing with exceptions are discussed later.
•final_suspend() defines whether the coroutine should be finally suspended. Here, we specify that we want to do so, which is usually the right thing to do. Note that this member function has to guarantee not to throw exceptions. It should always return std::suspend_always{}.
The purpose and use of these members of the promise type are discussed in detail later.