pure alias types

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
<>

1 Abstract

By 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.

2 Motivation

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.

The STL pure alias types are as follows.

Potentially future STL pure alias types are as follows.

2.1 The inconsistencies

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.

const int& x = (const int&)1;

// temporary for value 1 has same lifetime as x

std::initializer_list<int> x = { 1, 2, 3 };

// extends the lifetime of the array exactly like binding a reference to a temporary

std::span<const int> x = std::vector<int>{ 1, 2, 3 };

// immediate dangling

std::string_view bad = std::string{"a temporary string"};

// immediate dangling

std::string superfluous_alias{"a would be temporary string"};
auto rws = std::ref{superfluous_alias};

// 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.

3 The solution(s)

3.1 reference initialization + lifetime extension -> pure alias type initialization + lifetime extension

Reducing 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

const int& x = (const int&)1; // temporary for value 1 has same lifetime as x

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 c

Everything 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.

9.5.5 List-initialization [dcl.init.list] Paragraph 6 [N5054]

“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 dangling

In 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 dangling

This 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 dangling

NOTE: 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.

3.2 C++23 Simpler implicit move P2266R3

Simpler implicit move [P2266R3] made returning a reference to an xvalue ill formed.

int& h(bool b, int i) {
  static int s;
  if (b) {
    return s;  // OK
  } else {
    return i;  // error: i is an xvalue
  }
}

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

std::reference_wrapper<Widget> fifteen() {
    Widget w;
    return w;  // OK until CWG1579; OK after LWG2993. Proposed: ill-formed
}

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.

// -std=c++26
int& h(bool b, int i) {
  static int s;
  if (b) {
    return s;  // OK
  } else {
    return i;  // error: i is an xvalue
  }
}
// -std=c++26
const int& h(bool b, int i) {
  static int s;
  if (b) {
    return s;  // OK
  } else {
    return i;  // error: i is an xvalue
  }
}

return int&

x86-64 gcc 16.2

<source>: In function 'int& h(bool, int)':
<source>:7:12: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
    7 |     return i;  // error: i is an xvalue
      |            ^

x86-64 clang 22.1.0


<source>:7:12: error: non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
    7 |     return i;  // error: i is an xvalue
      |            ^

return const int&

x86-64 gcc 16.2

<source>: In function 'const int& h(bool, int)':
<source>:7:12: warning: reference to local variable 'i' returned [-Wreturn-local-addr]
    7 |     return i;  // error: i is an xvalue
      |            ^
<source>:2:26: note: declared here
    2 | const int& h(bool b, int i) {
      |                      ~~~~^

x86-64 clang 22.1.0

<source>:7:12: warning: reference to stack memory associated with parameter 'i' returned [-Wreturn-stack-address]
    7 |     return i;  // error: i is an xvalue
      |            ^

3.3 C++26 Disallow Binding a Returned Glvalue to a Temporary P2748R5

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.

const double& f2() {
    static int x = 42;
    return x;   // ill-formed
}

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.

3.4 C++26 Static storage for braced initializers P2752R3

initializer_list pure alias type was always lifetime extended.

9.5.5 List-initialization [dcl.init.list] Paragraph 6 [N5054]

“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:

void f(std::initializer_list<double> il);
void g(float x) {
  f({1, x, 3});
}
void h() {
  f({1, 2, 3});
}
// ...

The initializations can be implemented in a way roughly equivalent to this:

void g(float x) {
  const double __a[3] = {double{1}, double{x}, double{3}}; // backing array
  f(std::initializer_list<double>(__a, __a+3));
}
void h() {
  static constexpr double __b[3] = {double{1}, double{2}, double{3}}; // backing array
  f(std::initializer_list<double>(__b, __b+3));
}
// ...

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.

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?

void f(std::initializer_list<double> il);
void f(const double& il);

void h() {
  f({1}); // backing array MAY be static
  f(1); // nice if the single object was also static
}

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
}

3.5 eligible and ineligible for lifetime extension

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.

While 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.

3.5.1 eligible for lifetime extension

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.

A 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.

3.5.2 ineligible 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.

A 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.

4 Gradual adoption

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.

5 Impact on the standard

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.

References

[N5054] 2026. Working Draft, Programming Languages – C++.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/n5054.pdf
[P1974R1] 2026. Persistent constexpr allocation.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p1974r1.pdf
[P2266R3] 2022. Simpler implicit move.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2266r3.html
[P2748R5] 2024. Disallow Binding a Returned Glvalue to a Temporary.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2748r5.html
[P2752R3] 2023. Static storage for braced initializers.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2752r3.html
[P3086R5] 2025. Proxy: A Pointer-Semantics-Based Polymorphism Library.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3086r5.html
[P3274R0] 2024. A framework for Profiles development.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3274r0.pdf
[P3326R0] 2024. favor ease of use.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3326r0.html
[P3356R0] 2025. non_invalidating_vector.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3356r0.html
[P3390R0] 2024. Safe C++.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3390r0.html
[P3444R0] 2024. Memory Safety without Lifetime Parameters.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3444r0.html
[P4148R2] 2026. protocol: Structural Subtyping for C++.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4148r2.pdf