Why does as_const forbid rvalue arguments?
Asked Answered
C

4

19

I wanted to ask why as_const forbids rvalue arguments, according to cppreference.com (i.e. why the Standards folks made it so, not why cppreference.com specifically quoted them on that. And also not where in the spec the intent of the committee is codified, just for making sure :))). This (artificial) example would yield an error (user wants to make it const to keep COW quiet)

QChar c = as_const(getQString())[0];

Another question's answer notes that if we just remove the deletion of the rvalue reference overload, it would silently transform rvalues to lvalues. Right, but why not handle rvalues gracefully and return const rvalues for rvalue input and const lvalues for lvalue input?

Crownwork answered 20/8, 2016 at 7:3 Comment(12)
thanks, yes it's a dupe. closing ASAPCrownwork
ah, i'm not satisfied with the answer given there.. reopening! Please find my notes attached to the questionCrownwork
Why not have it like this https://mcmap.net/q/153390/-how-to-select-iterator-type-using-auto-variable .. I didn't see any immediate danger back then either.Crownwork
@Mehrdad i'm of the opinion that the above QChar example is a usecase. It prevents calling non-const member functions on rvalues. For COW-classes, even having a ref qualifier is not enough to prevent COW-ing, I think. The only safe way is having a const overload. And as_const comes right into the place to support that. Even if there seems no use case, what danger is there to support it? If there's a real danger, why not also forbid static_cast<const Foo&&>(getFoo()) then?Crownwork
(Sorry, deleted my comment since I basically put it into an answer.) But what I meant is this: why would you prevent calling a non-const method on an rvalue? Like, what error would it be trying to prevent? Can you give an example?Pneumatology
@Mehrdad a performance penalty due to copy on write (COW) in a non-const method overload.Crownwork
Interesting use case, but I think it's an implicit assumption that if a method has both a const and a non-const version, then they should both behave (reasonably) identically. The premise of COW is that you copy on writes, and calling a non-const version of a method whose const version is also available semantically shouldn't result in a write (because the const version logically wouldn't). So if that's how you're implementing COW, then your implementation is arguably wrong. Right?Pneumatology
One deeper reason could be that they want to discourage "magic calls". Even if it is legit under the hood, it can be misleading at first. It's like, having to think about both non-const and const implementation if you assume them to be different.Kilpatrick
@Mehrdad that's how it's done usually, though. COW doesn't need to perfectly detect when writes are done, it only needs to overapproximate it. It's cheap to detect it in the non-const overload. If the overloads return a naked pointer or a reference for instance, that's the only place for checking it. Otherwise you need to return a proxy object with op= overloaded.. which in all humbleness is quite an overkill :)Crownwork
Anyway, it was just an example. My gut feeling says that the language should grade power before safety. I'm a bit disappointed that in this case, they chose safety-firstCrownwork
as_const_ref could be added where it's allowed also for rvalues IMHO. The ref at the end would make it clear that it introduces an indirection.Crownwork
@JohannesSchaub-litb: If that's how it's usually done, I'd argue it's usually done wrong, and this is the overapproximation penalty for doing it wrong. =P But if you think about it, this is pretty consistent with what the language already does -- what you're asking for is basically like asking why doesn't C++ allow binding rvalues to lvalue references? It's power vs. safety there too, and they chose safety. Similar here. I'm not sure as_const_ref would be used frequently enough to justify it, so that's probably why they didn't do it. How often do people do COW (esp. an approximate one)?(!)Pneumatology
P
8

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.

Pneumatology answered 20/8, 2016 at 7:23 Comment(4)
Hm, that seems a compelling example of danger introduced! Thanks. Having it work for rvalues would be so useful, but I can understand that they don't want people to walk into this trap. Now I see that std::cref is also deleted for rvalue arguments, perhaps symmetry with it was another argument?Crownwork
@JohannesSchaub-litb: Well, sure, but that's probably getting the cause and effect backwards... I imagine that was also probably due to the same reason.Pneumatology
"lack of ownership transfer", what does that mean? I guess QString is COW implementation with brittle semantics where non-const item access induces preventive copying and const access does not but returns reference (that's very unsafe, sloppy design, but I'm assuming it). I still can't see what this example is about. I can imagine storing a pointer to whatever value refers to, a temporary or an item in the string's buffer, and that that pointer becomes dangling pretty fast. But that's nothing to do with as_const. Or is it?Parietal
Oh, the rvalue reference to const is bound to reference, so the temporary string's lifetime is not extended. I was misled by the "lack of ownership transfer" comment. What does that mean?Parietal
S
8

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.

Sochor answered 20/8, 2016 at 7:47 Comment(5)
Johannes' suggestion of as_const_ref would deal with this, wouldn't it? Then one wouldn't expect a lifetime extension.Parietal
@Cheersandhth.-Alf: As my concern is lifetime extension, indeed, if we stay with cases without life time extension requirement, as_const_ref seems good.Sochor
I think your alternative should say std::move(t)?Pneumatology
@user541686: std::forward<T>(t) used.Sochor
Ooh, good call. I didn't notice t might be an lvalue reference here!Pneumatology
Y
2

(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).

Yehudi answered 14/1, 2021 at 10:35 Comment(0)
T
-2

Because as_const doesn't take the argument as const reference. Non-const lvalue references can't bind to temporaries.

The forwarding reference overload is explicitly deleted.

Tendentious answered 20/8, 2016 at 7:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.