Why does as_const forbid rvalue arguments?

The problem is to handle lifetime extension

const auto& s = as_const(getQString()); // Create dangling pointer
QChar c = s[0]; // UB :-/

A possibility would be the following overload (instead of the deleted one)

template< typename T >
const T as_const(T&& t) noexcept(noexcept(T(std::forward<T>(t))))
    return std::forward<T>(t);

which involves extra construction, and maybe other pitfalls.

One reason might be that it could be dangerous on rvalues due to lack of ownership transfer

for (auto const &&value : as_const(getQString()))  // whoops!

and that there might not be a compelling use case to justify disregarding this possibility.

(I accidentally answered the wrong question to a related question of this Q&A, after mis-reading it; I'm moving my answer to this question instead, the question which my answer actually addressed)

P0007R1 introduced std::as_const as part of C++17. The accepted proposal did not mention rvalues at all, but the previous revision of it, P0007R0, contained a closing discussion on rvalues [emphasis mine]:

IX. Further Discussion

The above implementation only supports safely re-casting an l-value as const (even if it may have already been const). It is probably desirable to have xvalues and prvalues also be usable with as_const, but there are some issues to consider.


An alternative implementation which would support all of the forms used above, would be:

template< typename T >
inline const T &
as_const( const T& t ) noexcept
    return t;

template< typename T >
inline const T
as_const( T &&t ) noexcept( noexcept( T( t ) ) )
    return t;

We believe that such an implementation helps to deal with lifetime extension issues for temporaries which are captured by as_const, but we have not fully examined all of the implications of these forms. We are open to expanding the scope of this proposal, but we feel that the utility of a simple-to-use as_const is sufficient even without the expanded semantics.

So std::as_const was basically added only for lvalues as the implications of implementing it for rvalues were not fully examined by the original proposal, even if the return by value overload for rvalue arguments was at least visited. The final proposal, on the other hand, focused on getting the utility in for the common use case of lvalues.

P2012R0 aims to address the hidden dangers of range-based for loops

Fix the range‐based for loop, Rev0

The range-based for loop became the most important control structure of modern C++. It is the loop to deal with all elements of a container/collection/range.

However, due to the way it is currently defined, it can easily introduce lifetime problems in non-trivial but simple applications implemented by ordinary application programmers.


The symptom

Consider the following code examples when iterating over elements of an element of a collection:

std::vector<std::string> createStrings(); // forward declaration
for (std::string s : createStrings()) … // OK
for (char c : createStrings().at(0)) … // UB (fatal runtime error)

While iterating over a temporary return value works fine, iterating over a reference to a temporary return value is undefined behavior.


The Root Cause for the problem

The reason for the undefined behavior above is that according to the current specification, the range-base for loop internally is expanded to multiple statements: [...]

And the following call of the loop:

for (int i : createOptInts().value()) … // UB (fatal runtime error)

is defined as equivalent to the following:

auto&& rg = createOptInts().value(); // doesn’t extend lifetime of returned optional
auto pos = rg.begin();
auto end = rg.end();
for ( ; pos != end; ++pos ) {
 int i = *pos;

By rule, all temporary values created during the initialization of the reference rg that are not directly bound to it are destroyed before the raw for loop starts.


Severity of the problem


As another example for restrictions caused by this problem consider using std::as_const() in a range-based for loop:

std::vector vec; for (auto&& val : std::as_const(getVector())) { … }

Both std::ranges with operator | and std::as_const() have a deleted overload for rvalues to disable this and similar uses. With the proposed fix things like that could be possible. We can definitely discuss the usability of such examples, but it seems that there are more example than we thought where the problem causes to =delete function calls for rvalues.

These gotchas is one argument to avoid allowing an std::as_const() overload for rvalues, but if P2012R0 gets accepted, such an overload could arguably be added (if someone makes a proposal and shows a valid use case for it).