There are two more features in the C++26 library before moving on to concurrency: saturation arithmetic and debugging support.
Advertisement
Rainer Grimm has been working as a software architect, team and training manager for many years. He enjoys writing articles on the programming languages C++, Python and Haskell, but also frequently speaking at specialist conferences. On his blog Modern C++ he discusses his passion C++ in depth.
Saturation arithmetic
The English Wikipedia page describes saturation arithmetic as follows, Saturation arithmetic There is a version of Arithmetic in which all operations, such as Add And Multiplicationare limited to a certain range between the minimum and maximum value. If the result of an operation is greater than the maximum, it is set to (“clamped“) to the maximum; if it is below the minimum, it is clamped to the minimum. The name comes from how the value becomes “saturated” after reaching extreme values; adding more to the maximum or subtracting from the minimum will not change the result.
C++26 introduced several saturated arithmetic operations: addition, subtraction, multiplication, division, and saturating conversions. If the specified integer data type T cannot represent the result of the operation, the result is instead std::numeric_limits::min()
Or std::numeric_limits::max()
(whichever is closer).
cpppreference.com There is a good example with explanation as well std::add_sat:
#include
#include
#include
static_assert(CHAR_BIT == 8);
static_assert(UCHAR_MAX == 255);
int main()
{
constexpr int a = std::add_sat(3, 4); // no saturation occurs, T = int
static_assert(a == 7);
constexpr unsigned char b = std::add_sat(UCHAR_MAX, 4); // saturated
static_assert(b == UCHAR_MAX);
constexpr unsigned char c = std::add_sat(UCHAR_MAX, 4); // not saturated, T = int
// add_sat(int, int) returns int tmp == 259,
// then assignment truncates 259 % 256 == 3
static_assert(c == 3);
// unsigned char d = std::add_sat(252, c); // Error: inconsistent deductions for T
constexpr unsigned char e = std::add_sat(251, a); // saturated
static_assert(e == UCHAR_MAX);
// 251 is of type T = unsigned char, `a` is converted to unsigned char value;
// might yield an int -> unsigned char conversion warning for `a`
constexpr signed char f = std::add_sat(-123, -3); // not saturated
static_assert(f == -126);
constexpr signed char g = std::add_sat(-123, -13); // saturated
static_assert(g == std::numeric_limits::min()); // g == -128
}
T is the data type of both function arguments:
template< class T >
constexpr T add_sat( T x, T y ) noexcept;
Debugging Support
C++26 has three functions for debugging.
std::breakpoint
stops the running program when called and hands over control to the debugger,std::breakpoint_if_debugging
Callstd::breakpoint
when onstd::is_debugger_present
true
Return,std::is_debugger_present
Checks whether a program is running under the control of a debugger.
This was the first overview of the C++26 library. Let’s move on to Concurrency.
Concurrency is a major feature in C++26:
std::execution
std::execution
Previously called Sender/Receiver, provides a standard C++ framework for managing asynchronous execution on common resources (P2300R10). It has three essential elements: scheduler, sender, and receiver and provides a set of customizable asynchronous algorithms.
The “Hello World” program of the proposal P2300R10 She shows:
using namespace std::execution;
scheduler auto sch = thread_pool.scheduler(); // 1
sender auto begin = schedule(sch); // 2
sender auto hi = then(begin, (){ // 3
std::cout << "Hello world! Have an int."; // 3
return 13; // 3
}); // 3
sender auto add_42 = then(hi, ()(int arg) { return arg + 42; }); // 4
auto (i) = this_thread::sync_wait(add_42).value();
The explanation of the example is so good that I’ll quote it directly here:
This example demonstrates the basics of schedulers, senders, and receivers:
- First we need to get a scheduler from somewhere, such as a thread pool. The scheduler is a lightweight handle to the execution resource.
- To start a chain of jobs on the scheduler, we call § 4.19.1 Execution::Schedulewhich returns a sender that completes on the scheduler. A sender describes an asynchronous task and sends a signal (value, error, or halt) to some recipient when that task is complete.
- We use the sender algorithm to produce transmitters and write asynchronous tasks. § 4.20.2 execute::then There is a sender adapter that takes an input sender and a std::invocable, and calls the std::invocable on a signal sent by the input sender. The returned sender then sends the result of that invocation. In this case, the input sender came from the schedule, so it is void, which means it will not send us any value, so our std::invocable takes no parameters. But we return an int, which will be sent to the next recipient.
- Now, we add another operation to the chain by again using § 4.20.2 execute::then. This time, we are sent a value – the int from the previous step. We add 42 to it, and then return the result.
- Finally, we are ready to submit the entire asynchronous pipeline and wait for it to complete. Everything up to this point has been completely asynchronous; the work may not even have started yet. To ensure that the work has started and then block until it completes, we use § 4.21.1 this_thread::sync_waitwhich will return either a std:: option<:tuple>> with the value sent by the last sender, or an empty std::optional if the last sender sent a stop signal, or it throws an exception if the last sender sent an error.
Proposal There are other excellent examples. I will analyze them in upcoming articles.
Read-Copy Update (RCU) and Threat Indication
Read-copy updates and hazard pointers solve the classic problem of lock-free data structures such as lock-free stacks: when can a thread safely delete a data structure node while other threads may be using that node at the same time?
These structures are very specific and require too much information to be included in a review article.
What will happen next?
In the next article about C++26 I will change my focus and go into the details.
(RME)