Small Safety Improvements in the C++ 26 Core Language
Safety is an important concern in C++26. Contracts are probably the most important feature for safety. But C++26 has much more to offer.

Today, I would like to introduce three small but important improvements in C++26 that solve typical safety issues in C++.
Disallow binding a returned reference to a temporary
A safety issue can be challenging to find. This code snippet is from the proposal P2748R5, which I refer to throughout this section. I turned it into a minimal executable program.
// bindReferenceToTemporary.cpp #include <iostream> #include <string> #include <string_view> const std::string_view& getString() { static std::string s = "Hallo Welt!"; return s; } int main() { std::cout << getString() << '\n'; }
The GCC compiler already provides a meaningful error message:

Need another example? The following code snippet has a bug.
struct X { const std::map<std::string, int> d_map; const std::pair<std::string, int>& d_first; X(const std::map<std::string, int>& map) : d_map(map), d_first(*d_map.begin()) {} };
Unfortunately, the programmer overlooked the fact that the first element of the pair, known as the key, is constant. This creates a temporary variable. As a result, d_first
is no longer valid.
This brings us to the next safety improvement.
Erroneous Behaviour for Uninitialized Reads
Now I will refer to proposal p2795r5.
First of all, which objects does this apply to? These are all objects with automatic storage duration and temporaries. The special behaviour is that these objects are initialized to an arbitrary value. This means that the program has undefined behavior.
This, of course, leaves one question to be answered. What does automatic storage duration mean? The following variables have automatic storage duration :
- Variables that have a block scope and have not been explicitly declared as
static
,thread_local
, orextern
. The storage of these variables only remains valid until the block is completed. - Variables that belong to a parameter scope, such as a function. These are automatically destroyed when the parameter scope is dismantled.
Two examples from the proposal should clarify these words:
extern void f(int); int main() { int x; // default-initialized, value of x is indeterminate f(x); // glvalue-to-prvalue conversion has undefined behaviour }
void f(T&); void g(bool); void h() { T* p; // uninitialized, has erroneous value (e.g. null) bool b; // uninitialized, erroneous value may not be valid bool f(*p); // dereferencing has undefined behaviour g(b); // undefined behaviour if b is invalid }
To get to the point: the proposal turns uninitialized read operations, which represented undefined behavior with C++23, into erroneous programs with C++26.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
Of course, the complete initialization of automatic variables can be a relatively expensive operation. Therefore, there is an opt-out mechanism.
The [[indeterminate]]
Attribute
[[indeterminate]]
The [[indeterminate]]
attribute represents the opt-out mechanism. This feature is only intended for experts. The attribute allows variables with automatic storage that have not been initialized to be read without the program being erroneous. The following simplified example is from the proposal:
int x [[indeterminate]]; std::cin >> x; [[indeterminate]] int a, b[10], c[10][10]; compute_values(&a, b, c, 10); // This class benefits from avoiding determinate-storage initialization guards. struct SelfStorage { std::byte data_[512]; void f(); // uses data_ as scratch space }; SelfStorage s [[indeterminate]]; // documentation suggested this void g([[indeterminate]] SelfStorage s = SelfStorage()); // same; unusual, but plausible
The last safety feature is about incomplete types.
Deleting a Pointer to an Incomplete Type should be ill-formed
An incomplete type is a data type of which only the declaration exists, but no definition. Having a pointer or a reference to an incomplete type is perfectly OK. However, operations that require the size, layout, or member functions of this data type are erroneous.
The new feature is a little more specific: Deleting a pointer to an incomplete class type is ill-formed unless that class type has a trivial destructor and no class-specific deallocation function when completed. This means that the compiler created the destructor of the class, and operator delete
was not overloaded in the class.
The proposal provides a nice example to illustrate the subtle difference between a trivial destructor and a non-trivial destructor:
- Trivial destructor
// trivialDestructor.cpp namespace xyz { struct Widget; // forward decl Widget *new_widget(); } // namespace xyz int main() { xyz::Widget *p = xyz::new_widget(); delete p; } namespace xyz { struct Widget { const char *d_name; int d_data; // (implicit) trivial destructor // This is the only difference. }; Widget *new_widget() { return new Widget(); } } // namespace xyz
- Nontrivial destructor
// nontrivialDestructor.cpp namespace xyz { struct Widget; // forward decl Widget *new_widget(); } // namespace xyz int main() { xyz::Widget *p = xyz::new_widget(); delete p; } namespace xyz { struct Widget { const char *d_name; int d_data; ~Widget() {} // nontrivial dtor // This is the only difference. }; Widget *new_widget() { return new Widget(); } } // namespace xyz
The second example, nontrivialDestructor.cpp
, has a nontrivial destructor. In line with the proposal, compilation aborts with an error message:

What’s Next?
In my next article, I will take a look at some minor improvements to templates. These improvements are primarily intended to eliminate inconsistent behavior in C++.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, Honey Sukesan, bruce_lee_wayne, Silviu Ardelean, schnapper79, Seeker, and Sundareswaran Senthilvel.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | ![]() |
My special thanks to PVS-Studio | ![]() |
My special thanks to Tipi.build | ![]() |
My special thanks to Take Up Code | ![]() |
My special thanks to SHAVEDYAKS | ![]() |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org