The C++26 version of the C++ standard is a work in progress, but a series of language and library features have been already added. Furthermore, some of them are already supported by Clang and GCC. One of these new changes was discussed in my previous article, Erroneous behaviour has entered the chat. In this post, we will look at several language features added in C++26.
Specifying a reason for deleting a function
Since C++11, we can declare a function as deleted, so that the compiler will prevent its use. This can be used to prevent the use of class special member functions, but also to delete any other function. A function can be deleted as follows (example from the proposal paper):
class NonCopyable { public: // ... NonCopyable() = default; // copy members NonCopyable(const NonCopyable&) = delete; NonCopyable& operator=(const NonCopyable&) = delete; // maybe provide move members instead };
In C++26, you can specify a reason why this function is deleted:
class NonCopyable { public: // ... NonCopyable() = default; // copy members NonCopyable(const NonCopyable&) = delete("Since this class manages unique resources, copy is not supported; use move instead."); NonCopyable& operator=(const NonCopyable&) = delete("Since this class manages unique resources, copy is not supported; use move instead."); // provide move members instead };
The reason for having this feature is to help API authors to provide tailored messages for the removal of a function, instead of just relying on the generic compiler error for using a deleted function.
For more info see: P2573R2: = delete(“should have a reason”);
Placeholder variables with no name
There are cases when a variable has to be declared but its name is never used. An example is structure bindings. Another is locks (like lock_guard
), that are only used for their side-effects. In the future, another example could be pattern matching (for which several proposals exist).
In C++26, we can use a single underscore (_
) to define an unnamed variable.
For instance, in the following example, unused
is a variable that is not used:
[[maybe_unused]] auto [data, unused] = get_data();
In C++26, the unused
variable can be named _
(single underscore):
auto [data, _] = get_data();
When the single underscore identifier is used for the declaration of a variable, non-static class member variable, lambda capture, or structure binding, the [[maybe_unused]]
attribute is implicitly added, therefore, there is no need to explicitly use it.
A declaration with the name _
is said to be name-independent if it declares:
- a variable with automatic storage duration
- a structure binding, but not in a namespace scope
- a variable introduced by an init capture
- a non-static data member
The compiler will not emit warnings that a name-independent declaration is used or not. Moreover, multiple name-independent declarations can be used in the same scope (that is not a namespace scope):
int main() { int _; _ = 0; // OK std::string _; // OK, because _ is a name-independent declaration _ = "0"; // Error: ambiguous reference to placeholder '_', which is defined multiple times }
On the other hand, the following is not possible:
int main() { int _; _ = 0; // OK static std::string _; // Error: static variables are not name-independent }
The following is also not possible, because the declarations are in a namespace scope:
namespace n { int f() {return 42;} auto _ = f(); // OK auto _ = f(); // Error: redefinition of _ }
To learn more about this feature see: P2169: A nice placeholder with no name.
Structured binding declaration as a condition
A structure binding defines a set of variables that are bound to sub-objects or elements of their initializer.
auto [position, length] = get_next_token(text, offset);
A structure binding can appear in a for-range declaration, such as in the following example:
for (auto [position, length] : tokenize(text, offset)) { std::println("pos {}, len {}", position, length); }
On the other hand, variables can appear in the condition of an if
, while
, or for
statement:
if (auto it = std::find_if(begin(arr), end(arr), is_even); it != std::end(arr)) { std::println("{} is the 1st even number", *it); }
However, structure bindings cannot be declared in the condition of an if
, while
, or for
statement. That changes in C++26, which makes it possible:
if(auto [position, length] = get_next_token(text, offset); position >= 0) { std::println("pos {}, len {}", position, length); }
An interesting and very useful case is presented in the proposal paper (P0963). Consider the following C++26 example for using std::to_chars
:
if (auto result = std::to_chars(p, last, 42)) { auto [ptr, _] = result; // okay to proceed } else { auto [ptr, ec] = result; // handle errors }
When the function succeeds, we are only interested in the ptr
member of std::to_chars_result
, which contains a pointer to the one-past-the-end pointer of the characters written. If the function fails, then we also need to look at the ec
member (of the std::errc
type) representing an error code.
This code can be simplified with structure bindings, in C++26, as follows:
if (auto [ptr, ec] = std::to_chars(p, last, 42)) { // okay to proceed } else { // handle errors }
To learn more about this feature see: P0963: Structured binding declaration as a condition.
user-generated static_assert messages
The static_assert
‘s second parameter, which is a string representing the error message, can now be a compile-time user-generated string-like object. The following example uses a hypothetical constexpr std::format
, although this may also later appear in C++26:
static_assert(sizeof(int) == 4, std::format("Expected 4, actual {}", sizeof(int)));
To learn more about this feature see: P2471R3: user-generated static_assert messages.