Preventing Non-Const Lvalues from Resolving to Rvalue Reference Instead of Const Lvalue Reference

Preventing non-const lvalues from resolving to rvalue reference instead of const lvalue reference

When you have a templated function like this you almost never want to overload. The T&& parameter is a catch anything parameter. And you can use it to get any behavior you want out of one overload.

#include <iostream>
#include <vector>

using namespace std;

template <class T>
void display()
{
typedef typename remove_reference<T>::type Tr;
typedef typename remove_cv<Tr>::type Trcv;
if (is_const<Tr>::value)
cout << "const ";
if (is_volatile<Tr>::value)
cout << "volatile ";
std::cout << typeid(Trcv).name();
if (is_lvalue_reference<T>::value)
std::cout << '&';
else if (is_rvalue_reference<T>::value)
std::cout << "&&";
std::cout << '\n';
}

template <class T>
void foo(T&& t)
{
display<T>();
}

int main()
{
vector<int> x;
vector<int> const cx;
foo(x); // vector<int>&
foo(vector<int>()); // vector<int>
foo(cx); // const vector<int>&
}

initial value of reference to non-const must be an lvalue can be valid when returning from function?

The error message makes it quite clear:

initial value of reference to non-const must be an lvalue

(emphasis mine). So long as the reference is initially bound to an l-value, everything is fine (so long as you don't use a reference to a stack local variable, of course). The reference returned from get_value is bound to x which is an l-value, and that's allowed.

Without the function, you are essentially writing:

int x = 10;      // x is an l-value
int &get_x = x; // just a variable instead of a function
get_x = 20; // assignment is ok

which is clearly ok.

Why both const/nonconst lvalue references bind to a rvalue reference?

Rvalue references are implicitly converted to rvalues (more specifically, to xvalues) as one of the standard conversions (chapter 4 of the C++ standard):

The effect of any implicit conversion is the same as performing the
corresponding declaration and initialization and then using the
temporary variable as the result of the conversion. The result is an
lvalue if T is an lvalue reference type or an rvalue reference to
function type (8.3.2), an xvalue if T is an rvalue reference to object
type
, and a prvalue otherwise

Rvalues (including xvalues) can be bound to const lvalue references so that you can pass a temporary to a function with such a parameter:

void foo(const bar &a);
// ...
foo(bar());

(A temporary is an rvalue; in this case, the result of bar() is an rvalue). There is no reason not to allow this, since the temporary always lives as long as the containing expression - in this case, the function call - and so it will not create a dangling reference inside foo.

This means it is always possible to adjust the signature of a function fun(bar) to fun(const bar &) - and change the implementation accordingly of course! - since temporary arguments will still be accepted, and the semantics should be the same from the perspective of the caller; const implies the object won't be modified, which is also the case if it is passed by copy.

Non-const references are not allowed; one practical reason is because they imply that the value should be modified in some meaningful way, and if the value is a temporary, this would be lost. However, you can convert an rvalue to an lvalue if you really want to do so, with some caveats, as described in this answer to another question.

Allowing an rvalue to bind to a const lvalue reference, other than allowing temporary arguments to be passed by reference, is also good for cases where the exact parameter type is not known but you want to allow move semantics if it is possible. Suppose that I am calling a function that could be defined as foo2(const bar &) or foo2(bar) and which in the former case may or may not have an overload foo2(bar &&), and I want to allow move semantics to be used if possible (assuming that a foo2(bar &&) overload will use move semantics in its implementation); I can safely use std::move to create an rvalue since it will apply in either case. This example might seem a little contrived, but it is the sort of thing that can come up quite a bit when writing templates. In code:

bar bb = bar();
foo2(std::move(bb));
// above is legal if foo2 is declared as either:
// foo2(bar)
// foo2(const bar &)
// and in latter case calls overload foo2(bar &&) if it is defined.

In the case of other rvalue-to-lvalue-reference assignments involving a temporary, the lifetime of the temporary is extended to that of the reference, so that dangling references are not created even in contexts other than parameter passing:

const bar &b = bar(); // temporary lifetime is extended

In the above, the bar object will not be destroyed until the reference b goes out of scope.

Why can an rvalue not bind to a non-const lvalue reference, other than the fact that writing to a temporary has no effect?

The simple answer is that in most cases, passing a temporary to a function that expects a mutable lvalue reference indicates a logic error, and the c++ language is doing its best to help you avoid making the error.

The function declaration: void foo(Bar& b) suggests the following narrative:

foo takes a reference to a Bar, b, which it will modify. b is therefore both an input and an output

Passing a temporary as the output placeholder is normally a much worse logic error than calling a function which returns an object, only to discard the object unexamined.

For example:

Bar foo();

void test()
{
/*auto x =*/ foo(); // probable logic error - discarding return value unexamined
}

However, in these two versions, there is no problem:

void foo(Bar&& b)

foo takes ownership of the object referenced by Bar

void foo(Bar b)

foo conceptually takes a copy of a Bar, although in many cases the compiler will decide that creating and copying a Bar is un-necessary.

So the question is, what are we trying to achieve? If we just need a Bar on which to work we can use the Bar&& b or Bar b versions.

If we want to maybe use a temporary and maybe use an existing Bar, then it is likely that we would need two overloads of foo, because they would be semantically subtly different:

void foo(Bar& b);    // I will modify the object referenced by b

void foo(Bar&& b); // I will *steal* the object referenced by b

void foo(Bar b); // I will copy your Bar and use mine, thanks

If we need this optionality, we can create it by wrapping one in the other:

void foo(Bar& b)
{
auto x = consult_some_value_in(b);
auto y = from_some_other_source();
modify_in_some_way(b, x * y);
}

void foo(Bar&& b)
{
// at this point, the caller has lost interest in b, because he passed
// an rvalue-reference. And you can't do that by accident.

// rvalues always decay into lvalues when named
// so here we're calling foo(Bar&)

foo(b);

// b is about to be 'discarded' or destroyed, depending on what happened at the call site
// so we should at least use it first
std::cout << "the result is: " << b.to_string() << std::endl;
}

With these definitions, these are now all legal:

void test()
{
Bar b;
foo(b); // call foo(Bar&)

foo(Bar()); // call foo(Bar&&)

foo(std::move(b)); // call foo(Bar&&)
// at which point we know that since we moved b, we should only assign to it
// or leave it alone.
}

OK, by why all this care? Why would it be a logic error to modify a temporary without meaning to?

Well, imagine this:

Bar& foo(Bar& b)
{
modify(b);
return b;
}

And we're expecting to do things like this:

extern void baz(Bar& b);

Bar b;
baz(foo(b));

Now imagine this could compile:

auto& br = foo(Bar());

baz(br); // BOOM! br is now a dangling reference. The Bar no longer exists

Because we are forced to handle the temporary properly in a special overload of foo, the author of foo can be confident that this mistake will never happen in your code.

Why rvalue reference argument matches to const reference in overload resolution?

This behaviour is attributed to overload resolution rules. As per standard 8.5.3/p5.2 References [dcl.init.ref], rvalue references bind to const lvalue references. In this example:

std::data(T())

You provide to std::data an rvalue. Thus, due to overload resolution rules the overload:

template <class _Cont> constexpr
auto data(const _Cont& __c) -> decltype(__c.data()) { return __c.data(); }

is a better match. Consequently you get const int*

You can't bind a temporary to a non-const lvalue reference.

Allowed to bind an rvalue to a non-const lvalue reference?

This is a compiler "extension" (or "bug", depending on your perspective) of the Microsoft compiler. C++ only allows non-const binding of an lvalue to a non-const lvalue reference.

binding const reference to rvalue reference

If you want to preserve the lifetime semantics of a return value depending on the value category of the returned expression, you can't return a const&, or even a && since you will face issues with dangling references.

Instead, you can use decltype(auto) for the return type in order to deduce the appropriate value category of the returned expression:

template <typename Task, typename Priority>
decltype(auto) Heap<Task, Priority>::priority_of(const size_t& index) const
{
decltype(auto) result = vec.at(index).second;
return decltype(result)(result);
}

Now the return type will deduce the correct value-category, i.e. l-values for l-value references, and r-values for pr-values (temporaries), and x-values (expiring values).

The cast to decltype(result) in the return statement is used to cast the expression to the appropriate type based on the type of the entity named by the id-expression result.

You need to use this technique for all functions in the call stack where you want to preserve the lifetime semantics.

You can think of this technique as perfect-forwarding, but in the opposite direction, i.e. up the call stack, instead of down.

This answer is based on the technique described in this entertaining lightning talk.

Returning const reference to temporary behaves differently than local const reference?

Given const Val &foo = test(Val(5));, the temporary Val(5) will be destroyed after the full expression immediately, its lifetime won't be extended to the lifteime of the reference foo. It's not bound to foo directly, but bound to the reference parameter of test.

In reference initialization,

(emphasis mine)

Whenever a reference is bound to a temporary or to a subobject
thereof, the lifetime of the temporary is extended to match the
lifetime of the reference, with the following exceptions:

  • a temporary bound to a reference parameter in a function call exists until the end of the full expression containing that function call: if
    the function returns a reference, which outlives the full expression,
    it becomes a dangling reference.

In general, the lifetime of a temporary cannot be further extended by
"passing it on": a second reference, initialized from the reference to
which the temporary was bound, does not affect its lifetime.



Related Topics



Leave a reply



Submit