| Document #: | P4344R0 [Latest] [Status] |
| Date: | 2026-08-13 |
| Project: | Programming Language C++ |
| Audience: |
CWG SG17 EWG Incubator SG23 Safety and Security |
| Reply-to: |
Jarrad J. Waterloo <descender76@gmail.com> |
-> pure alias type initialization +
lifetime extensionBy formalizing pure alias types, we remove seeming
inconsistencies between references and other alias types. This will
result in fewer instances of immediate dangling, return based dangling
and superfluous aliasing.
Pure alias types are types that are composed of only
pure alias types. Instances of these types refers to other
object(s), they do not own them.
The core pure alias types are as follows.
*&return_type (*)(parameter_type_1, parameter_type_2, parameter_type_3)member_type class_type::*variable_name = &class_type::member_name;return_type (class_type::*)(parameter_type_1, parameter_type_2, parameter_type_3)The STL pure alias types are as follows.
Potentially future STL pure alias types are as
follows.
How we deal with temporaries in the STL is all over the
place! Under ideal circumstances, we benefit from a lifetime extension
that allows us to create pure alias types simply in the
language. If programmers try to do that in other plausable scenarios, we
are rewarded with immediate dangling. Still, in other places, we make
our API(s) more difficult to work with by banning temporaries even in
scenarios where it wouldn’t be a problem such as value returning
functions instead of pure alias type returning
functions.
| ✓ |
|
// temporary for value 1 has same lifetime as x |
| ✓ |
|
// extends the lifetime of the array exactly like binding a reference to a temporary |
| ✗ |
|
// immediate dangling |
| ✗ |
|
// immediate dangling |
| ✓✗ |
|
// superfluous aliasing because std::reference_wrapper bans temporaries, not even conditionally [P3326R0] |
NOTE: The scope of this proposal is about making the core language more consistent, not to revise any specific temporary banning library to take advantage of these changes.
-> pure alias type initialization +
lifetime extensionReducing inconsistencies between pure alias types and
temporaries could provide significant value.
|
7.2 Properties of expressions [expr.prop] [N5054] 7.2.1 Value category [basic.lval] … 8 [Note 8 : The discussion of reference initialization in 9.5.4 and of temporaries in 6.8.7 indicates the behavior of lvalues and rvalues in other significant contexts. — end note] |
The first inconsistency of interest has to do with interaction of reference initialization and temporary lifetime extension.
|
6.8.7 Temporary objects [class.temporary] [N5054] … C.1.4 Clause 9: declarations [diff.cpp23.dcl.dcl] Paragraph 2 |
This scenario is rarely utilized because it can be written more simply as follows.
const int x = 1;Even if you remove the cast, (const int&), the
initial & is superfluous.
const int& x = 1;Another, more critical, problem with lifetime extension of temporaries for reference initialization is that it is not composable.
const int& a = (const int&)1; // temporary for value 1 has same lifetime as a
const int& b = (const int&)1; // temporary for value 1 has same lifetime as b
const int& c = (const int&)1; // temporary for value 1 has same lifetime as cEverything works fine here but the moment you aggregate
a, b and c then you run into
immediate dangling, even though the aggregate of a,
b and c is a pure alias type just
like & is.
class composition
{
public:
composition(const int& _a, const int& _b, const int& _c) : a{_a}, b{_b}, c{_c} {}
//private:
const int& a;
const int& b;
const int& c;
};
composition c{1, 1, 1}; // immediate dangling of 3 temporaries
c.a + c.b + c.c; // use after free of all 3 temporaries
// This would be so even if the three reference members
// was replaced with three pointers or
// three reference_wrappers.With this proposal, those three instances of immediate dangling would
be fixed with no errors or warning messages by lifetime extending the
three temporaries used in the construction of the
pure alias type. This is consistent with the existing
lifetime extension afforded to the existing &
pure alias type. It is also consistent with the existing
lifetime extension afforded to the existing
initializer_list pure alias type.
|
“The backing array has the same lifetime as any other temporary object (6.8.7), except that initializing an initializer_list object from the array extends the lifetime of the array exactly like binding a reference to a temporary.” |
This would also eliminate certain examples of immediate dangling
associated with existing STL types such as
string_view.
string_view sv1 = "Hello World";// no immediate dangling
string_view sv2 = string{"Hello World"};// immediate danglingIn order to deal with this now, programmers have to identify and manually eliminate the temporary.
string_view sv1 = "Hello World";// no immediate dangling
string some_undesired_alias{"Hello World"};
string_view sv2 = some_undesired_alias;// no immediate danglingThis results in superfluous aliasing. Not only is this code not as
terse as it could be, superfluous aliases are not desired by those who
believe in enforcing unique mutable aliasing. [P3390R0] [P3444R0]
Besides reducing superfluous aliasing, this would also be indirectly beneficial for dealing with some invalidation concerns because it allows programmers to more succintly use non invalidating views of objects, whether these views are created manually or via future reflection capabilities.
span<string> s = vector<string>{"Hello", "World"};// immediate dangling + ill formed
// span<const string> s = vector<string>{"Hello", "World"};// immediate dangling + incorrect constness
// non_invalidating_view<vector<string>> s = vector<string>{"Hello", "World"};// P3356R0 + immediate danglingNOTE: The previous example would need an implicit conversion added to vector that produces a span that preserves the same constness used for span as that was used for vector. That library tweak is out of scope of the requested lifetime extension.
Using existing non invalidating views or hand crafted ones does
mitigate data races simply, once we have a lifetime extension supporting
this scenario. This greatly reduces the need of an owning type such as
gsl::dyn_array being added.
|
A framework for Profiles development [P3274R0] Why 99%? Well, I could have said 90% and still made the point. What might be an example? … Many, but not all, … data races can be handled by passing unique_ptrs and containers without invalidating operations (e.g., gsl::dyn_array rather than vector) together with the simplest static anti-aliasing checks. |
Simpler implicit move [P2266R3] made returning a reference to an
xvalue ill formed.
|
Simpler implicit move [P2266R3] was always meant to support other
pure alias types besides reference, such as
reference_wrapper. However, no explicit wording was added
for reference_wrapper.
|
3.4. A specific case involving reference_wrapper … |
Disallowing returning a pure alias to xvalues in general
would further reduce return based dangling.
It should be noted that Simpler implicit move [P2266R3] seems to be a
work in progress as adding const to the returned reference
changes the error into a warning. It would seem that some static
analysis in two of our major compilers is allowing a static analysis
warning to supercede a core error.
|
|
|
return x86-64 gcc 16.2 x86-64 clang 22.1.0 return x86-64 gcc 16.2 x86-64 clang 22.1.0 |
|
Disallow Binding a Returned Glvalue to a Temporary [P2748R5] pretty much says
that returning a reference to a temporary is always a bug and as such
was made ill-formed.
|
Disallow Binding a Returned Glvalue to a Temporary [P2748R5] This change was so uncontroversial because binding a reference to a temporary, when the reference will outlive the temporary and become dangling as soon as the full-expression completes, is always a bug. … |
The fact that this is always a bug is true whether the temporary was created implicitly as seen above or explicitly as seen below.
const std::string& f2() {
return std::string{"Hello World"}; // ill-formed
}So, why should this ill-formed requirement be limited to
& pure alias types? Isn’t the following,
also, always a bug.
const std::string_view f2() {
return std::string{"Hello World"}; // always a bug
}Consequently, we should disallow binding a returned pure alias type to a temporary.
initializer_list pure alias type was always
lifetime extended.
|
“The backing array has the same lifetime as any other temporary object (6.8.7), except that initializing an initializer_list object from the array extends the lifetime of the array exactly like binding a reference to a temporary.” |
Static storage for braced initializers [P2752R3] extends the
lifetime of the backing array further to have static storage duration,
when all of the elements of the initializer_list are not
dynamically initialized.
|
Static storage for braced initializers [P2752R3] [Example 12: The initializations can be implemented in a way roughly equivalent to this: |
It should noted that while revisions to the standard did provide an example demonstrating the conditions under which this effect takes affect, there was no corresponding wording stating such. Consequently, programmers truly have no idea the lifetime of the backing array because the standard has implicitly left it undefined. How can a programmer be responsible for dangling if there are not clear rules with respect to lifetime? Guaranteeing static storage duration would make the existing feature more consistent with string literals.
|
5.13.5 String literals [lex.string] 9 Evaluating a string-literal results in a string literal object with static storage duration (6.8.6). |
Leaving this implicit compiler defined behavior with respect to lifetimes in the standard begs for the following questions to be answered.
initializer_list, doesn’t that mean every C++11 compiler
that implements initializer_list is already conforming to
the C++26 standard?Static storage for braced initializers is not called
Static storage for initializer_list. Rather the intent was
about braced initializers which equally apply to single objects as it
does a backing array of single objects. Why should an
initializer_list of one object have static storage duration
and not just the object itself?
|
NOTE: Making a single const object const initialized is already permitted in the standard.
|
6.10.3.2 Static initialization [basic.start.static] … 3 An implementation is permitted to perform the initialization of a variable with static or thread storage duration as a static initialization even if such initialization is not required to be done statically, provided that (3.1) — the dynamic version of the initialization does not change the value of any other object of static or thread storage duration prior to its initialization, and (3.2) — the static version of the initialization produces the same value in the initialized variable as would be produced by the dynamic initialization if all variables not required to be initialized statically were initialized dynamically. |
Also, it doesn’t necessarily disallow optimizations as constant with static storage duration can be an object in many different known and unknown ways.
Programmers are asking for more static storage durations guarantees as they are usually held responsible for dangling, though we don’t always know the lifetimes of objects, as currently the standard leaves too much of this as undefined.
The following three instances illustrates how this relates to
pure alias types. Besides required static storage duration
guarantees, the following example could promote all three temporaries to
have static storage duration since the objects were not dynamically
initialized and because pure alias type is `const. This
would eliminate these instances of dangling. While not required,
deduplicating these 3 temporary instances would eliminate 2 dynamic
memory allocations. Any one of these eliminations of dynamic memory
allocations would supercede in performance of any real or imagined
machine opcode optimization. This can further be optimized in the future
by placing the shared instance in read only memory. [P1974R1] Neither
optimization is needed for programmers to enjoy the consistency,
simplicity and freedom from these instances of dangling.
const std::vector<int>& x1 = std::vector<int>{ 1, 2, 3 };
const std::span<int> x2 = std::vector<int>{ 1, 2, 3 };// library const fix needed
const non_invalidating_view<vector<int>> x3 = std::vector<int>{ 1, 2, 3 };Guaranteed static storage duration for pure alias types
would also simplify things by transforming errors produced by
Simpler implicit move [P2266R3] and
Disallow Binding a Returned Glvalue to a Temporary [P2748R5] into valid
code.
const int& f1()
{
return 42;// safe with static storage duration, can further be optimized
}
const std::string& f2()
{
return std::string{"42"};// safe with static storage duration, can further be optimized
}
const std::vector<std::string>& f3()
{
return std::vector<std::string>{std::string{"42"}};// safe with static storage duration, can further be optimized
}
const std::span<int> f4()
{
return std::vector<int>{42};// safe with static storage duration, can further be optimized
}This proposal started by talking about a new
type category associated with
pure alias types. The proposed lifetime extension only
kicks in only when all of the following specific conditions are met.
NOTE: The proposed lifetime extension does not kick in if the
pure alias type is being initialized by a function that is
not a constructor or initializer_list because the former could be
refering to existing objects while the later is concerning object
creation.
This section floats the idea of two additional type categories.
eligible for lifetime extensionineligible for lifetime extensionWhile it may be unknown whether a type fits into either of these two
type categories, a type can’t be part of both
type categories. That would be ill-formed.
Types that are eligible for lifetime extension are types
that if lifetime extended would not change the semantic behavior of a
program. Every type that is trivially destructible is also
eligible for lifetime extension because their destructor
need not be called and only the deallocating the storage for the object
needs be performed. In that sense,
eligible for lifetime extension is an extended form of
trivially destructible by allowing for the destruction of dynamically
allocated memory. A non exhaustive list of types that falls into this
category is as follows.
pure alias typeseligible for lifetime extension
eligible for lifetime extensioneligible for lifetime extensioneligible for lifetime extensionA type may be conditionally
eligible for lifetime extension. For instance, most
containers would only be eligible for lifetime extension if
their primary template arguments were also
eligible for lifetime extension.
Types that are ineligible for lifetime extension are
types that should never be lifetime extended because doing so would
change the semantic behavior of a program. A non exhaustive list of
types that falls into this category is as follows.
ineligible for lifetime extensionA type may be conditionally
ineligible for lifetime extension. For instance, a null
mutex frequently has no state, resulting in an empty destuctor and
public member methods that do nothing. Consequently, a type that uses a
null mutex as a template type parameter would be
eligible for lifetime extension, while if that same type
uses null mutex’s cousin, mutex, would be
ineligible for lifetime extension.
Having both eligible for lifetime extension and
ineligible for lifetime extension aids in removing the
limitation that pure alias types can only work with
constructors and initializer lists. Functions, in general, that
initialize a pure alias type can benefit from lifetime
extensions. If the type of the temporary passed to a function is
eligible for lifetime extension than it would be lifetime
extended. Otherwise, it would not be lifetime extended if the type of
the temporary was ineligible for lifetime extension or is
unknown. The latter is the current default behavior.
This could be refined further if a lifetime bound annotation was
supported by only lifetime extending when the type of the temporary is
eligible for lifetime extension and the parameter type was
annotated as lifetime bound. Also, if a parameter type is lifetime bound
and the type of the provided temporary is
ineligible for lifetime extension than an error should be
produced.
Nothing proposed constitutes a breaking change and as such could be
released complete. However, if time does not permit, this proposal could
be rolled out incrementally by gradually refining the definition of
pure alias type.
Phase #1: pure alias type just represents those types in
the core language and those in the STL.
Phase #2: pure alias type represents Phase #1 and
compiler deduced of user defined aggregations of
pure alias type.
Phase #3: pure alias type represents Phase #2 and user’s
can explicity and conditional state whether a type is a
pure alias type or not. This allows for 3rd party
variations of span because it contains a length member which is not a
pure alias type.
Phase #4: pure alias type represents Phase #3,
eligible for lifetime extension and
ineligible for lifetime extension.
All of the proposed solutions reduces immediately dangling and return based dangling code, which is always invalid. With the proposed revisions, the code becomes valid, errors appropriately and makes sense from a developer’s perspective.