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?

Community
  • 1
  • 1
Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • thanks, yes it's a dupe. closing ASAP – Johannes Schaub - litb Aug 20 '16 at 07:08
  • ah, i'm not satisfied with the answer given there.. reopening! Please find my notes attached to the question – Johannes Schaub - litb Aug 20 '16 at 07:09
  • Why not have it like this http://stackoverflow.com/a/9453435/34509 .. I didn't see any immediate danger back then either. – Johannes Schaub - litb Aug 20 '16 at 07:19
  • @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(getFoo())` then? – Johannes Schaub - litb Aug 20 '16 at 07:24
  • (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? – user541686 Aug 20 '16 at 07:25
  • @Mehrdad a performance penalty due to copy on write (COW) in a non-const method overload. – Johannes Schaub - litb Aug 20 '16 at 07:32
  • 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? – user541686 Aug 20 '16 at 07:37
  • 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. – Frederik.L Aug 20 '16 at 07:39
  • @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 :) – Johannes Schaub - litb Aug 20 '16 at 07:39
  • 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-first – Johannes Schaub - litb Aug 20 '16 at 07:42
  • `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. – Johannes Schaub - litb Aug 20 '16 at 07:43
  • @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)?(!) – user541686 Aug 20 '16 at 07:46

4 Answers4

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.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
7

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.

user541686
  • 205,094
  • 128
  • 528
  • 886
  • 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? – Johannes Schaub - litb Aug 20 '16 at 07:31
  • @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. – user541686 Aug 20 '16 at 07:40
  • "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? – Cheers and hth. - Alf Sep 20 '16 at 16:16
  • 1
    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? – Cheers and hth. - Alf Sep 20 '16 at 16:22
1

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

dfrib
  • 70,367
  • 12
  • 127
  • 192
-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.

user2393256
  • 1,001
  • 1
  • 15
  • 26