Why Is It Allowed to Pass R-Values by Const Reference But Not by Normal Reference

Why is it allowed to pass R-Values by const reference but not by normal reference?

For your final question:

how can a const reference keep pointing to an R-Value (anonymous variable)

Here is the answer. The C++ language says that a local const reference prolongs the lifetime of temporary values until the end of the containing scope, but saving you the cost of a copy-construction (i.e. if you were to use an local variable instead).

C++11 rvalue reference vs const reference

std::move doesn't actually move anything out of it's own. It's just a fancy name for a cast to a T&&. Calling test like this test(std::move(x)); only shows that a T&& is implicitly convertible to a const T&. The compiler sees that test only accepts const T& so it converts the T&& returned from std::move to a const T&, that's all there is to it.

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.

Passing non-const references to rvalues in C++

Yes, the fact that plain functions cannot bind non-const references to temporaries -- but methods can -- has always bugged me. TTBOMK the rationale goes something like this (sourced from this comp.lang.c++.moderated thread):

Suppose you have:

 void inc( long &x ) { ++x; }

void test() {
int y = 0;
inc( y );
std::cout << y;
}

If you allowed the long &x parameter of inc() to bind to a temporary long copy made from y, this code obviously wouldn't do what you expect -- the compiler would just silently produce code that leaves y unchanged. Apparently this was a common source of bugs in the early C++ days.

Had I designed C++, my preference would have been to allow non-const references to bind to temporaries, but to forbid automatic conversions from lvalues to temporaries when binding to references. But who knows, that might well have opened up a different can of worms...

Why does the const qualifier allow temporary objects or rvalues?

Lvalues represent objects and rvalues represent values (loosely speaking). Binding an rvalue to a non-const lvalue reference would allow you to modify the value - what exactly does that mean? If you had written a = 3 in your function, you'd be attempting to modify the value 2 to make it 3. That just doesn't make sense conceptually. However, const lvalue references do not allow modification and only allow you to observe a value, which does make sense.

However, there often are object behind rvalue expressions and it can be useful to modify those objects (such as to steal its resources). This is precisely why the rvalue reference was introduced.

Difference between ordinary parameter, reference parameter and const reference parameter passed by ordinary object and object created temporary

It's pretty simple:

  1. ctor 1 receives the argument by copy (pass by value);
  2. ctor 2 receives the argument by non-const lvalue reference, so it only supports non-const lvalues.
  3. ctor 3 receives argument by const lvalue reference so it supports const-lvalue, non-const lvalue, const rvalue and non-const rvalue.

Instantiation 1 does not actually create a variable, but a temporary value for initializing cat, so it is not suitable as a reference parameter.

Yes, it creates a prvalue, and it can be passed by reference like this:

// ctor 4
Cat( std::string&& n ) // see the double ampersand

ctor 4 only accepts temporaries (rvalues) by reference.

For constructor 3, the const reference parameter represents a constant that can accept a temporary value for initialization

Yes, it can bind to all the values as I mentioned previously.

For Constructor 2:
Instantiation 1 raises an error. The compiler complains that 'Candidate constructor not viable: expects an lvalue for 1st argument'

This is the expected behavior.

Important Note:

Cat(std::string&& n): name(n) { // here n is of type rvalue refernece to string
std::printf("Call: Cat(std::string& n)\n");
}

Function parameters that are of type rvalue reference are lvalues themselves. So in the above code, n is a variable and it's an lvalue. Thus it has an identity and can not be moved from (you need to use std::move in order to make it movable).

Extra note:

You can even pass an lvalue to a function that only accepts rvalues like this:

Cat( std::string&& n );

std::string s("Mike");
Cat cat2( std::move(s) );

std::move performs a simple cast. It casts an lvalue to an xvalue so it becomes usable by a function that only accepts an rvalue.

Value Category in C++11

Take a look at this:
Value Categories in C++11

Explanation for the above image

In C++11, expressions that:

  • have identity and cannot be moved from are called lvalue expressions;
  • have identity and can be moved from are called xvalue expressions;
  • do not have identity and can be moved from are called prvalue ("pure rvalue") expressions;
  • do not have identity and cannot be moved from are not used[6].

The expressions that have identity are called "glvalue expressions" (glvalue stands for "generalized lvalue"). Both lvalues and xvalues are glvalue expressions.

The expressions that can be moved from are called "rvalue expressions". Both prvalues and xvalues are rvalue expressions.

Read more about this topic at value categories.

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.

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.



Related Topics



Leave a reply



Submit