The latest C++ Standard has recently been finalized and compiler support, especially in Clang, is progressing rapidly. (The code in this article was tested on Clang 23 with -std=c++2c, while all of the sample error messages are also from Clang.) This article adds to previous ones on this site about C++26 and aims to demonstrate four new additions to the language (as opposed to the Standard Library) that will make your code cleaner and safer.
1. Reason for = delete
Modern C++ allows for marking member functions as = delete as a better way of disallowing their use than declaring them as private: while specific free function overloads and template instantiation candidates can also be decorated in this way.
C++26 extends this idea and removes the need to leave a comment in the code for future reference as instead a custom diagnostic error message can be output in case of attempted usage of the function by other code:
#include <utility>
class NoMove {
public:
NoMove() = default;
NoMove(const NoMove&) = default;
NoMove& operator=(const NoMove&) = default;
NoMove(NoMove&&) = delete("This class does not support move-construction!");
NoMove& operator=(NoMove&&) = delete("This class does not support move-assignment!");
virtual ~NoMove() = default;
// ...
};
int main() {
auto nm = NoMove();
auto nm2 = std::move(nm);
nm = std::move(nm2);
}
In the above code the syntax = delete("MESSAGE"); is used on the move constructor and move copy assignment operator of a pretend class called NoMove (which we pretend can’t be moved, possibly due to it containing a non-movable data member).
Attempting to compile this program gives the output:
test_delete.cpp:16:10: error: call to deleted constructor of 'remove_reference_t<NoMove &>' (aka 'NoMove'): This class
does not support move-construction!
16 | auto nm2 = std::move(nm);
| ^ ~~~~~~~~~~~~~
test_delete.cpp:8:5: note: 'NoMove' has been explicitly marked deleted here
8 | NoMove(NoMove&&) = delete("This class does not support move-construction!");
| ^
test_delete.cpp:17:8: error: overload resolution selected deleted operator '=': This class does not support
move-assignment!
17 | nm = std::move(nm2);
| ~~ ^ ~~~~~~~~~~~~~~
test_delete.cpp:7:13: note: candidate function
7 | NoMove& operator=(const NoMove&) = default;
| ^
test_delete.cpp:9:13: note: candidate function has been explicitly deleted
9 | NoMove& operator=(NoMove&&) = delete("This class does not support move-assignment!");
| ^
2 errors generated.
Notice that the messages each appear twice, both as part of the error diagnostic and also as output of the source code itself.
As mentioned, free functions can also be = delete with a message:
#include <cmath>
template<typename T>
T f(const T v) = delete("Overload for this parameter type is not supported.");
template<>
double f(const double v) {
return cos(v) / sin(v);
}
template<>
long double f(const long double v) {
return cosl(v) / sinl(v);
}
template<>
float f(const float v) = delete("Float overload does not provide sufficient precision.");
int main() {
auto a = f(2.0);
auto b = f(2.0L);
auto c = f(2.0f); // Error: attempt to use deleted instantiation
auto d = f(2); // Error: attempt to use unsupported overload
}
(Currently, Clang support appears incomplete due to not repeating the error message for the deleted instantiation.)
test_delete2.cpp:22:14: error: call to deleted function 'f'
22 | auto c = f(2.0f); // Error: attempt to use deleted instantiation
| ^
test_delete2.cpp:17:7: note: candidate function [with T = float] has been explicitly deleted
17 | float f(const float v) = delete("Float overload does not provide sufficient precision.");
| ^
test_delete2.cpp:23:14: error: call to deleted function 'f': Overload for this parameter type is not supported.
23 | auto d = f(2); // Error: attempt to use unsupported overload
| ^
test_delete2.cpp:4:3: note: candidate function [with T = int] has been explicitly deleted
4 | T f(const T v) = delete("Overload for this parameter type is not supported.");
| ^
2 errors generated.
2. Placeholder _
The use of an “unnamed variable” usually called _ (underscore) is commonplace in other languages, either as a convention or as a language feature. C++26 borrows this use of _ as a variable which can be redefined in the same scope multiple times, and reassigned to with a value or the most recent type defined for it. This sounds quirky but makes a lot more sense with some code to study:
#include <tuple>
#include <print>
auto getData() {
return std::tuple{ 1, 2.3, "Hello!"};
}
int main() {
auto [ a, _, b] = getData();
std::println("{}:{}", a, b);
auto c = _; // Okay, can dereference placeholder `_` as type double
auto _ = "Again."; // Okay, placeholder '_' can be defined multiple times with different types
// auto d = _; // Error: ambiguous reference to placeholder '_', which is defined multiple times
}
Notice the _ in the destructuring assignment from getData(), which will be of type double at this point. It is possible to assign from the unnamed variable if it has only been defined once in the current scope (this prevents breaking code which uses it as an actual variable name).
The line auto _ = "Again."; redefines the unnamed variable (with type const char[7]) and from this point on it cannot be dereferenced. This behavior is more intuitive and in line with other programming languages.
3. Disallowing temporaries as returned references
It is perfectly legal C++ to assign temporaries to const-references, but problems may surface if it is used as a function’s return value. The following code compiles on C++23, but generates an error message on C++26:
const int& f(int i) {
const auto &j = i * 2;
return j;
}
test_nodangling.cpp:3:12: error: returning reference to local temporary object
3 | return j;
| ^
test_nodangling.cpp:2:17: note: binding reference variable 'j' here
2 | const auto &j = i * 2;
| ^ ~~~~~
1 error generated.
Bugs related to dangling references are not always easy to reproduce across builds, and you should be aware that this safety feature of the language is not an opt-in, so may fail to compile code which compiled without issues (and even ran correctly) previously.
4. Compile-time selected diagnostics for static_assert()
The static_assert(condition, "message"); syntax has been around since C++11, with the message part made optional since C++17 (use of static_assert(false); is an idiomatic way to halt compilation in a particular file). With C++26 we can build the message as desired (of course this means using constexpr classes such as std::string or std::string_view).
This program simulates the build module of a larger code project, with the global free function buildType() created in some fashion by the external build system:
#include <string_view>
using namespace std::literals;
enum BuildType : int { Debug = 1, Asserts = 2, Release = 4, Optimized = 8 };
constexpr auto noOptimizedDebug = "Cannot make an Optimized Debug build!"sv;
constexpr auto noDebugRelease = "Cannot combine Debug and Release for build!"sv;
constexpr BuildType buildType() {
return BuildType(Debug | Optimized);
}
int main() {
constexpr auto b = buildType();
static_assert(
(b != 9) && ((b & 5) != 5),
b == 9 ? noOptimizedDebug
: (b & 5) == 5 ? noDebugRelease
: "Unrecognized build error!"sv
);
}
In this form, the following diagnostic is output:
test_static_assert.cpp:18:10: error: static assertion failed due to requirement '(b & 9) != 9': Cannot make an Optimized
Debug build!
18 | ((b & 9) != 9) && ((b & 5) != 5),
| ^~~~~~~~~~~~
test_static_assert.cpp:18:18: note: expression evaluates to '9 != 9'
18 | ((b & 9) != 9) && ((b & 5) != 5),
| ~~~~~~~~^~~~
1 error generated.
Use of the ternary operator (test ? case true : case false) in the message portion was not previously possible, and other schemes for generating the error message “dynamically” (at compile time) are also possible.
Summary
Hopefully you’ve gained some ideas from this article about how to improve new C++ code you write and also to refactor existing code as and when you upgrade your compiler. These features of C++26 are small and self-contained, ideal for making them accepted and widely utilized. They also showcase the incremental ways in which the language can be improved while maintaining full compatibility with existing codebases (feature 3. above is the exception).