![](/user_photo/_userpic.png)
- •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
126 |
Chapter 6: Ranges and Views |
However, this optimization has significant consequences:
•You might not be able to iterate over views when they are const.
•Concurrent iterations can cause undefined behavior even if we do not modify anything.
•Early read access might invalidate or change the later iterations over elements of views.
We will discuss all of this later in detail. For the moment, note that the standard views are considered harmful when modifications happen:
•Inserting or removing elements in the underlying range may have significant impact on the functionality of a view. After such modification, a view might behave differently or even no longer be valid.
Therefore, it is strongly recommended to use views right before you need them. Create views to use them ad hoc. If modifications happen between initializing a view and using it, care has to be taken.
6.1.4 Sentinels
To deal with ranges, we have to introduce a new term, sentinels, which represent the end of a range.
In programming, a sentinel is a special value that marks an end or termination. Typical examples are:
•The null terminator ’\0’ as the end of a character sequence (e.g., used in string literals)
•nullptr marking the end of a linked list
•-1 to mark the end of a list of non-negative integers
In the ranges library, sentinels define the end of a range. In the traditional approach of the STL, sentinels would be the end iterators, which usually have the same type as the iterators that iterate over a collection. However, with C++20 ranges, having the same type is no longer required.
The requirement that end iterators have to have the same type as the iterators that define the begin of a range and that are used to iterate over the elements causes some drawbacks. Creating an end iterator may be expensive or might not even be possible:
•If we want to use a C string or string literal as a range, we first have to compute the end iterator by iterating over the characters until we find ’\0’. Thus, before we use the string as a range, we have already done the first iteration. Dealing then with all the characters would need a second iteration.
•In general, this principle applies if we define the end of a range with a certain value. If we need an end iterator to deal with the range, we first have to iterate over the whole range to find its end.
•Sometimes, iterating twice (once to find the end and then once to process the elements of the range) is not possible. This applies to ranges that use pure input iterators, such as using input streams that we read from as ranges. To compute the end of the input (may be EOF), we already have to read the input. Reading the input again is not possible or will yield different values.
The generalization of end iterators as sentinels solves this dilemma. C++20 ranges support sentinels (end iterators) of different types. They can signal “until ’\0’”, “until EOF”, or until any other value. They can even signal “there is no end” to define endless ranges and “hey, iterating iterator, check yourself whether there is the end.”
![](/html/75672/2303/html_Pld0VJ4wzF.QW__/htmlconvd-7poybx153x1.jpg)
6.1 A Tour of Ranges and Views Using Examples |
127 |
Note that before C++20, we could also have these kinds of sentinels but they were required to have the same type as the iterators. One example was input stream iterators: a default constructed iterator of type std::istream_iterator<> was used to create an end-of-stream iterator so that you could process input from a stream with an algorithm until end-of-file or an error occurs:
// print all int values read from standard input: |
|
std::for_each(std::istream_iterator<int>{std::cin}, |
// read ints from cin |
std::istream_iterator<int>{}, |
// end is an end-of-file iterator |
[] (int val) { |
|
std::cout << val << '\n'; |
|
}); |
|
By relaxing the requirement that sentinels (end iterators) now have to have the same type as iterating iterators, we gain a couple of benefits:
•We can skip the need to find the end before we start to process. We do both together: process the values and find the end while iterating.
•For the end iterator, we can use types that disable operations that cause undefined behavior (such as calling the operator *, because there is no value at the end). We can use this feature to signal an error at compile time when we try to dereference an end iterator.
•Defining an end iterator becomes easier.
Let us look at a simple example using a sentinel of a different type to iterate over a “range” where the types
of the iterators differ: |
|
|
|
||
ranges/sentinel1.cpp |
|
|
#include <iostream> |
|
|
#include <compare> |
|
|
#include <algorithm> |
// for for_each() |
struct NullTerm {
bool operator== (auto pos) const {
return *pos == '\0'; // end is where iterator points to ’\0’
}
};
int main()
{
const char* rawString = "hello world";
// iterate over the range of the begin of rawString and its end:
for (auto pos = rawString; pos != NullTerm{}; ++pos) { std::cout << ' ' << *pos;
}
std::cout << '\n';
// call range algorithm with iterator and sentinel: std::ranges::for_each(rawString, // begin of range
![](/html/75672/2303/html_Pld0VJ4wzF.QW__/htmlconvd-7poybx154x1.jpg)
128 |
|
Chapter 6: Ranges and Views |
||
|
NullTerm{}, // end is null terminator |
|
|
|
|
|
|
|
|
|
[] (char c) { |
|
|
|
|
std::cout << ' ' << c; |
|
|
|
|
}); |
|
|
|
std::cout << '\n'; |
|
|
|
|
} |
|
|
|
|
The program has the following output: |
|
|
||
|
|
|||
h e l l o |
w o r l d |
|
|
|
h e l l o |
w o r l d |
|
|
|
In the program, we first define an end iterator that defines the end as having a value equal to ’\0’:
struct NullTerm {
bool operator== (auto pos) const {
return *pos == '\0'; // end is where iterator points to ’\0’
}
};
Note that we combine a few other new features of C++20 here:
•When defining the member function operator==(), we use auto as the parameter type. That way we make the member function generic so that the operator == can compare with an object pos of arbitrary type (provided comparing the values that pos refers to with ‘\0‘ is valid).
•We define only the operator == as a generic member function even though algorithms usually compare iterators with sentinels using the operator !=. Here, we benefit from the fact that C++20 can now map the operator != to the operator == with an arbitrary order of the operands.
Using Sentinels Directly
We then use a basic loop to iterate over the “range” of characters of the string rawString:
for (auto pos = rawString; pos != NullTerm{}; ++pos) { std::cout << ' ' << *pos;
}
We initialize pos as an iterator that iterates over the characters and prints out their values with *pos. The loop runs while its comparison with NullTerm{} yields that the value of pos does not equal ’\0’. That way, NullTerm{} serves as a sentinel. It does not have the same type as pos but it supports a comparison with pos in such a way that it checks the current value that pos refers to.
Here you can see how sentinels are the generalization of end iterators. They may have a different type than the iterator that iterates over the elements, but they support comparisons with the iterator to decide whether we are at the end of a range.
6.1 A Tour of Ranges and Views Using Examples |
129 |
Passing Sentinels to Algorithms
C++20 provides overloads for algorithms that no longer require that the begin iterator and the sentinel (end iterator) have the same type. However, these overloads are provided in the namespace std::ranges:
std::ranges::for_each(rawString, |
// begin of range |
NullTerm{}, |
// end is null terminator |
... ); |
|
The algorithms in the namespace std still require that the begin iterator and the end iterator have the same type and cannot be used in that way:
std::for_each(rawString, NullTerm{}, // ERROR: begin and end have different types
... );
If you have two iterators of different types, type std::common_iterator provides a way to harmonize them for traditional algorithms. This can be useful because numeric algorithms, parallel algorithms, and containers still require that the begin and the end iterator have the same type.
6.1.5Range Definitions with Sentinels and Counts
Ranges can be more than just containers or a pair of iterators. Ranges can be defined by:
•A begin iterator and an end iterator of the same type
•A begin iterator and a sentinel (an end marker of maybe a different type)
•A begin iterator and a count
•Arrays
The ranges library supports the definition and use of all of these ranges.
First, the algorithms are implemented in such a way that the ranges can be arrays. For example:
int rawArray[] = {8, 6, 42, 1, 77};
...
std::ranges::sort(rawArray); // sort elements in the raw array
In addition, there are several utilities for defining ranges defined by iterators and sentinels or counts, and these are introduced in the following subsections.
Subranges
To define ranges of iterators and sentinels, the ranges library provides type std::ranges::subrange<>. Let us look at a simple example using subranges:
|
|
||
ranges/sentinel2.cpp |
|
|
|
#include <iostream> |
|
|
|
#include <compare> |
|
|
|
#include <algorithm> |
// for for_each() |
|
|
struct NullTerm { |
|
|
|
bool operator== (auto pos) const { |
|
||
return *pos == '\0'; // end is where iterator points to ’\0’ |
|
||
} |
|
|
|
}; |
|
|
|
|
|
|
|
![](/html/75672/2303/html_Pld0VJ4wzF.QW__/htmlconvd-7poybx156x1.jpg)
130 |
Chapter 6: Ranges and Views |
int main()
{
const char* rawString = "hello world";
//define a range of a raw string and a null terminator: std::ranges::subrange rawStringRange{rawString, NullTerm{}};
//use the range in an algorithm:
std::ranges::for_each(rawStringRange, |
|
[] (char c) { |
|
std::cout << ' ' << c; |
|
}); |
|
std::cout << '\n'; |
|
// range-based for loop also supports iterator/sentinel: |
|
for (char c : rawStringRange) { |
|
std::cout << ' ' << c; |
|
} |
|
std::cout << '\n'; |
|
} |
|
As introduced as an example of a sentinel we define type NullTerm as a type for sentinels that check for the |
|
null terminator of a string as the end of a range. |
|
By using a std::ranges::subrange, the program defines a range object that represents the beginning |
|
of the string and the sentinel as its end: |
|
std::ranges::subrange rawStringRange{rawString, NullTerm{}}; |
|
A subrange is the generic type that can be used to convert a range defined by an iterator and a sentinel into |
|
a single object that represents this range. In fact, the range is even a view, which internally, just stores the |
|
iterator and the sentinel. This means that subranges have reference semantics and are cheap to copy. As a subrange, we can pass the range to the new algorithms that take ranges as single arguments:
std::ranges::for_each(rawStringRange, ... ); // OK
Note that subranges are not always common ranges, meaning that calling begin() and end() for them may yield different types. Subranges just yield what was was passed to define the range.
Even when a subrange is not a common range, you can pass it to a range-based for loop. The rangebased for loop accepts ranges where the types of the begin iterator and the sentinel (end iterators) differ (this feature was already introduced with C++17, but with ranges and views, you can really benefit from it):
for (char c : std::ranges::subrange{rawString, NullTerm{}}) { std::cout << ' ' << c;
}
![](/html/75672/2303/html_Pld0VJ4wzF.QW__/htmlconvd-7poybx157x1.jpg)
6.1 A Tour of Ranges and Views Using Examples |
131 |
We can make this approach even more generic by defining a class template where you can specify the value that ends a range. Consider the following example:
ranges/sentinel3.cpp
#include <iostream> #include <algorithm>
template<auto End> struct EndValue {
bool operator== (auto pos) const {
return *pos == End; // end is where iterator points to End
}
};
int main()
{
std::vector coll = {42, 8, 0, 15, 7, -1};
//define a range referring to coll with the value 7 as end: std::ranges::subrange range{coll.begin(), EndValue<7>{}};
//sort the elements of this range:
std::ranges::sort(range);
|
// print the elements of the range: |
|
|
std::ranges::for_each(range, |
|
|
[] (auto val) { |
|
|
std::cout << ' ' << val; |
|
|
}); |
|
|
std::cout << '\n'; |
|
|
// print all elements of coll up to -1: |
|
|
std::ranges::for_each(coll.begin(), EndValue<-1>{}, |
|
|
[] (auto val) { |
|
|
std::cout << ' ' << val; |
|
|
}); |
|
|
std::cout << '\n'; |
|
|
||
} |
|
|
Here, we define EndValue<> as the end iterator, checking for the end passed as a template parameter. EndValue<7>{} creates an end iterator where 7 ends the range and EndValue<-1>{} creates an end iterator where -1 ends the range.
The output of the program is as follows:
0 8 15 |
42 |
|
0 8 15 42 |
7 |
132 |
Chapter 6: Ranges and Views |
You could define a value of any supported non-type template parameter type.
As another example of sentinels, look at std::unreachable_sentinel. This is a value that C++20 defines to represent the “end” of an endless range. It can help you to optimize code so that it never compares against the end (because that comparison is useless if it always yields false).
For more aspects of subranges, see the details of subranges.
Ranges of Begin and a Count
The ranges library provides multiple ways of dealing with ranges defined as beginning and a count.
The most convenient way to create a range with a begin iterator and a count is to use the range adaptor std::views::counted(). It creates a cheap view to the first n elements of a begin iterator/pointer.
For example:
std::vector<int> coll{1, 2, 3, 4, 5, 6, 7, 8, 9};
auto pos5 = std::ranges::find(coll, 5);
if (std::ranges::distance(pos5, coll.end()) >= 3) { for (int val : std::views::counted(pos5, 3)) {
std::cout << val << ' ';
}
}
Here, std::views::counted(pos5, 3) creates a view that represents the three elements starting with the element to which pos5 refers. Note that counted() does not check whether there are elements (passing a count that is too high results in undefined behavior). It is up to the programmer to ensure that the code is valid. Therefore, with std::ranges::distance(), we check whether there are enough elements (note that this check can be expensive if your collection does not have random-access iterators).
If you know that there is an element with the value 5 that has at least two elements behind it, you can also write:
// if we know there is a 5 and at least two elements behind:
for (int val : std::views::counted(std::ranges::find(coll, 5), 3)) { std::cout << val << ' ';
}
The count may be 0, which means that the range is empty.
Note that you should use counted() only when you really have an iterator and a count. If you already have a range and want to deal with the first n elements only, use std::views::take().
For more details, see the description of std::views::counted().