Consider the following:
struct my_type {};
my_type make_my_type() { return my_type{}; }
void func(my_type&& arg) {}
int main()
{
my_type&& ref = make_my_type();
func(ref);
}
Needless to say, this code doesn't compile. I realise that I need to use std::move() in the second function call, but for the purposes of understanding I want to consider the code as it is.
Attempting to compile the above, Clang 3.5 tells me:
error: no matching function for call to 'func'
note: candidate function not viable: no known conversion from 'my_type' to 'my_type &&' for 1st argument void func(my_type&&) {}
While g++ 4.9 says something almost identical:
error: cannot bind 'my_type' lvalue to 'my_type&&'
note: initializing argument 1 of 'void func(my_type&&)'
These error messages have me rather confused, because while ref is certainly an lvalue, its type is still my_type&&... isn't it?
I'm trying to understand exactly what's going on here, so I'm wondering which (if any) of the following are true:
Since only rvalues can be bound to rvalue references, and ref is an lvalue, it cannot be bound to arg. The error messages from both Clang and g++ are misleading in claiming that ref is a (non-reference) my_type that "cannot be converted".
Because it is an lvalue, ref is treated for the purposes of overload resolution as a non-reference my_type, despite its actual type being my_type&&. The error messages from Clang and g++ are misleading because they are displaying the type as used internally for function matching, not the real type of ref.
In the body of main(), the type of ref is plain my_type, despite the fact I explicitly wrote my_type&&. So the error messages from the compilers are accurate, and it is my expectation that is wrong. This doesn't seem to be the case however, since
static_assert(std::is_same<decltype(ref), my_type&&>::value, "");
passes.
There is some other magic going on that I haven't considered.
Just to repeat, I know that the solution is to use std::move() to convert the rref back into an rvalue; I'm looking for an explanation of what's going on "behind the scenes".
Consider these three assignments:
my_type x = func_returning_my_type_byvalue();
my_type & y = func_returning_my_type_byvalue();
my_type && z = func_returning_my_type_byvalue();
The first - you have a local variable x and it's being initialized to the result of a function call (rvalue) so a move constructor/assignment can be used or the construction of x could be elided entirely (skipped over and x is constructed in-place by func_returning_my_type_byvalue when it generates its result).
Note that x is an lvalue - you can take its address, so therefore it is also a type of reference itself. Technically all variables that are not references, are references to themselves. In that respect lvalues are a binding site for assignments to and reads from known-storage-duration memory.
The second will not compile - you cannot assign a reference to a result (this way), you must use reference assignment syntax to alias an existing lvalue. It is perfectly fine to do this, however:
my_type & y = func_returning_my_type_byreference();
// `y` will never use constructors or destructors
This is why the third exists, when we need a reference to something we cannot create a reference to using the conventional syntax. Within something like func in the original question, the lifetime of arg is not immediately obvious. For example we can't do this without an explicit move:
void func( my_type && arg ) {
my_type && save_arg = arg;
}
The reason this is not allowed is because arg is a reference to a value first and foremost. If the storage of arg's value (what it's referring to) were to be shorter than that of save_arg, then save_arg would call the destructor of that value - in effect capturing it. That is not the case here, save_arg will disappear first, so it makes no sense to transfer an lvalue into it that we can, after func, still refer to potentially!
Consider that even if you were to use std:move to force this to compile. The destructor will still not be called within func because you haven't created a new object, just a new reference, and then this reference is destroyed before the original object itself went out of scope.
For all intents and purposes arg behaves as if it's my_type&, as do any rvalue references. The trick is storage duration and the semantics of lifetime extension by reference passing. It's all regular references under the hood, there is no 'rvalue type'.
If it helps, recall the increment / decrement operators. There are two overloads that exist, not two operators. operator++(void) (pre) and operator++(int) (post). There is never an actual int being passed, it's just so the compiler has different signatures for different situations / contexts / agreements about value treatment. This is sort of the same deal with references.
If rvalue and lvalue references are both always referred to like an lvalue, what's the difference?
In a word: object lifetime.
An lvalue reference must always be assigned to using something with longer storage duration, something that is already constructed. That way there is no need to call constructors or destructors for the scope of the lvalue reference variable, because by definition we are given a ready object and forget about it before it's due to be destroyed.
it's also relevant that objects are implicitly destroyed in the reverse order they're defined:
int a; // created first, destroyed last
int b; // created second, destroyed 2nd-last
int & c = b; // fine, `c` goes out of scope before `b` per above
int && d = std::move(a); // fine, `a` outlives `d`, same situation as `c`
If we assigned to an rvalue reference, something that is an lvalue reference, the same rule applies - the lvalue must by definition have longer storage, so we don't need to call constructors or destructors for c or even d. You can't trick the compiler with std::move on this because it knows the scope of the object being moved - d is unambiguously shorter-duration than the reference it's being given, we're just forcing the compiler to use the rvalue type check / context and that's all we've achieved.
The difference is with non-lvalue references - things like expressions where there can be references to them but these references are definitely short-lived, perhaps shorter than the duration of a local variable. Hint Hint.
When we assign the result of a function call or an expression to an rvalue reference, we are creating a reference to a temporary object that otherwise could not be referred to. Due to this, we are in effect forcing in-place construction of a variable from the result of an expression. This is a variation on copy/move elision where the compiler has no choice but to elide the temporary to in-place construction:
int a = 2, b = 3; // lvalues
int && temp = a + b; // temp is constructed in-place using the result of operator+(int,int)
The case with func
It boils down to an lvalue assignment - references as function arguments refer to objects that may exist for longer than a function call, and as such are lvalues even when the argument type is an rvalue reference.
The two cases are:
func( std::move( variable ) ); // case 1
func( my_type() + my_type() ); // case 2
func is not allowed to guess which situation we will use it in ahead of time (sans optimizations). If we didn't allow case 1, then there would be a legitimate reason to consider an rvalue reference parameter as having less storage duration than the function call, but that would also make no sense because either the object is always cleaned up inside func or always outside of it, and having "unknown" storage duration at compile time is not satisfactory.
The compiler has no choice but to assume the worst, that case 1 might happen eventually, in which case we must make guarantees to the storage duration of arg as being longer than the call to func in the general case. As consequence of this - that arg would be considered to exist for longer than the call to func some of the time, and that func's generated code must work in both cases - arg's allowable usage and assumed storage duration meet the requirements of my_type& and not my_type&&.
Related
This is a very minimal example:
class Foo
{
public:
Foo(int x) {};
};
void ProcessFoo(Foo& foo)
{
}
int main()
{
ProcessFoo(Foo(42));
return 0;
}
The above compiles fine on Visual Studio, but generates an error on Linux and Mac.
Compiling the above generates this:
$ g++ -std=c++11 -c newfile.cpp
newfile.cpp: In function ‘int main()’:
newfile.cpp:23:23: error: invalid initialization of non-const reference of type ‘Foo&’ from an rvalue of type ‘Foo’
ProcessFoo(Foo(42));
^
newfile.cpp:14:6: note: in passing argument 1 of ‘void ProcessFoo(Foo&)’
void ProcessFoo(Foo& foo)
I've found three workarounds:
Create a temp variable for the invocation of ProcessFoo.
Like this:
Foo foo42(42);
ProcessFoo(foo42);
ProcessFoo takes a const reference: void ProcessFoo(const Foo& foo)
ProcessFoo just lets Foo get passed by value. void ProcessFoo(Foo foo)
Why is the compiler forbidding my original code? (What is it guarding against)? What is it about each of the three workarounds above that satisfies the compiler? What would MSVC allow it, but not g++?
By design, C++ only allows a temporary to be passed to a const reference, value, or rvalue reference. The idea is that a function taking a non-const reference parameter is stating that it wants to modify the parameter and allowing it to go back to the caller. Doing so with a temporary is meaningless and most likely an error.
And I don't know what version of g++ you're running. It doesn't work here: http://coliru.stacked-crooked.com/a/43096cb398cbc973
Why is the compiler forbidding my original code?
Because it is forbidden by the Standard:
8.5.3 References 5
...
Otherwise, the reference shall be an lvalue reference to a non-volatile const type (i.e., cv1 shall be
const), or the reference shall be an rvalue reference.
[ Example:
double& rd2 = 2.0; // error: not an lvalue and reference not const
...
'
What is it guarding against?
Inadvertently modifying an object that is going to be destructed after the function call.
What is it about each of the three workarounds above that satisfies the compiler?
1 Creates a named object and 3 a copy.
2 Works because the lifetime of the object is simply extended, and changes to it are prevented at the same time.
What would MSVC allow it, but not g++?
Because it is a language extension. Disable it by going to Property Pages->C/C++->Language->Disable Language Extensions and you'll get an error.
Why is the compiler forbidding my original code?
MSVC has an extension that allows temporaries to bind to non-const lvalue-references. Of course this isn't a standard-conforming feature so I would stay away from it to be portable. For example, it doesn't work with the latest versions of GCC and Clang as you've seen.
What is it about each of the three workarounds above that satisfies the compiler?
Back in C++03, expressions could only be lvalues or rvalues. References could only designate the "lvalueness" of an object, and so it was used with the intention of aliasing a preexisting object. By contrast, rvalues don't exist beyond the expression in which they appear. Also, the end result of references was normally to copy or modify the object, and it doesn't make much sense to the language to modify an rvalue like 55 for example.
The rules allow you to bind an rvalue to a lvalue-reference to const, in which case the temporary's lifetime is extended to the lifetime of the reference. When you take an object by value the object is copied.
With C++11 we have rvalue-references and xvalues which were made for the purpose of exchanging ownership. With this is lessens the usefulness of lvalue-references to const. Moreover, taking by-value causes a move if it is an rvalue.
Once you declared the prototype for ProcessFoo as
void ProcessFoo(Foo& foo)
You are conveying your intent as the formal parameter "foo" is subject to modification as it is not being passed by const &.
At the call-site,
ProcessFoo(Foo(42));
Foo(42) is creating a temporary stack object that is not modifiable. It is okay to pass-by-value or pass-by-ref-to-const to a method.
As you listed yourself, satisfying those constraints, makes the compiler happy.
is giving you an object that is not compiler generated and is under your control.
Informs the compiler that the method guarantees const-ness of the object.
Informs the compiler that this (temporary) object is passed by value and hence no issues.
I think there's something I'm not quite understanding about rvalue references. Why does the following fail to compile (VS2012) with the error 'foo' : cannot convert parameter 1 from 'int' to 'int &&'?
void foo(int &&) {}
void bar(int &&x) { foo(x); };
I would have assumed that the type int && would be preserved when passed from bar into foo. Why does it get transformed into int once inside the function body?
I know the answer is to use std::forward:
void bar(int &&x) { foo(std::forward<int>(x)); }
so maybe I just don't have a clear grasp on why. (Also, why not std::move?)
I always remember lvalue as a value that has a name or can be addressed. Since x has a name, it is passed as an lvalue. The purpose of reference to rvalue is to allow the function to completely clobber value in any way it sees fit. If we pass x by reference as in your example, then we have no way of knowing if is safe to do this:
void foo(int &&) {}
void bar(int &&x) {
foo(x);
x.DoSomething(); // what could x be?
};
Doing foo(std::move(x)); is explicitly telling the compiler that you are done with x and no longer need it. Without that move, bad things could happen to existing code. The std::move is a safeguard.
std::forward is used for perfect forwarding in templates.
Why does it get transformed into int once inside the function body?
It doesn't; it's still a reference to an rvalue.
When a name appears in an expression, it's an lvalue - even if it happens to be a reference to an rvalue. It can be converted into an rvalue if the expression requires that (i.e. if its value is needed); but it can't be bound to an rvalue reference.
So as you say, in order to bind it to another rvalue reference, you have to explicitly convert it to an unnamed rvalue. std::forward and std::move are convenient ways to do that.
Also, why not std::move?
Why not indeed? That would make more sense than std::forward, which is intended for templates that don't know whether the argument is a reference.
It's the "no name rule". Inside bar, x has a name ... x. So it's now an lvalue. Passing something to a function as an rvalue reference doesn't make it an rvalue inside the function.
If you don't see why it must be this way, ask yourself -- what is x after foo returns? (Remember, foo is free to move x.)
rvalue and lvalue are categories of expressions.
rvalue reference and lvalue reference are categories of references.
Inside a declaration, T x&& = <initializer expression>, the variable x has type T&&, and it can be bound to an expression (the ) which is an rvalue expression. Thus, T&& has been named rvalue reference type, because it refers to an rvalue expression.
Inside a declaration, T x& = <initializer expression>, the variable x has type T&, and it can be bound to an expression (the ) which is an lvalue expression (++). Thus, T& has been named lvalue reference type, because it can refer to an lvalue expression.
It is important then, in C++, to make a difference between the naming of an entity, that appears inside a declaration, and when this name appears inside an expression.
When a name appears inside an expression as in foo(x), the name x alone is an expression, called an id-expression. By definition, and id-expression is always an lvalue expression and an lvalue expressions can not be bound to an rvalue reference.
When talking about rvalue references it's important to distinguish between two key unrelated steps in the lifetime of a reference - binding and value semantics.
Binding here refers to the exact way a value is matched to the parameter type when calling a function.
For example, if you have the function overloads:
void foo(int a) {}
void foo(int&& a) {}
Then when calling foo(x), the act of selecting the proper overload involves binding the value x to the parameter of foo.
rvalue references are only about binding semantics.
Inside the bodies of both foo functions the variable a acts as a regular lvalue. That is, if we rewrite the second function like this:
void foo(int&& a) {
foo(a);
}
then intuitively this should result in a stack overflow. But it doesn't - rvalue references are all about binding and never about value semantics. Since a is a regular lvalue inside the function body, then the first overload foo(int) will be called at that point and no stack overflow occurs. A stack overflow would only occur if we explicitly change the value type of a, e.g. by using std::move:
void foo(int&& a) {
foo(std::move(a));
}
At this point a stack overflow will occur because of the changed value semantics.
This is in my opinion the most confusing feature of rvalue references - that the type works differently during and after binding. It's an rvalue reference when binding but it acts like an lvalue reference after that. In all respects a variable of type rvalue reference acts like a variable of type lvalue reference after binding is done.
The only difference between an lvalue and an rvalue reference comes when binding - if there is both an lvalue and rvalue overload available, then temporary objects (or rather xvalues - eXpiring values) will be preferentially bound to rvalue references:
void goo(const int& x) {}
void goo(int&& x) {}
goo(5); // this will call goo(int&&) because 5 is an xvalue
That's the only difference. Technically there is nothing stopping you from using rvalue references like lvalue references, other than convention:
void doit(int&& x) {
x = 123;
}
int a;
doit(std::move(a));
std::cout << a; // totally valid, prints 123, but please don't do that
And the keyword here is "convention". Since rvalue references preferentially bind to temporary objects, then it's reasonable to assume that you can gut the temporary object, i.e. move away all of its data away from it, because after the call it's not accessible in any way and is going to be destroyed anyway:
std::vector<std::string> strings;
string.push_back(std::string("abc"));
In the above snippet the temporary object std::string("abc") cannot be used in any way after the statement in which it appears, because it's not bound to any variable. Therefore push_back is allowed to move away its contents instead of copying it and therefore save an extra allocation and deallocation.
That is, unless you use std::move:
std::vector<std::string> strings;
std::string mystr("abc");
string.push_back(std::move(mystr));
Now the object mystr is still accessible after the call to push_back, but push_back doesn't know this - it's still assuming that it's allowed to gut the object, because it's passed in as an rvalue reference. This is why the behavior of std::move() is one of convention and also why std::move() by itself doesn't actually do anything - in particular it doesn't do any movement. It just marks its argument as "ready to get gutted".
The final point is: rvalue references are only useful when used in tandem with lvalue references. There is no case where an rvalue argument is useful by itself (exaggerating here).
Say you have a function accepting a string:
void foo(std::string);
If the function is going to simply inspect the string and not make a copy of it, then use const&:
void foo(const std::string&);
This always avoids a copy when calling the function.
If the function is going to modify or store a copy of the string, then use pass-by-value:
void foo(std::string s);
In this case you'll receive a copy if the caller passes an lvalue and temporary objects will be constructed in-place, avoiding a copy. Then use std::move(s) if you want to store the value of s, e.g. in a member variable. Note that this will work efficiently even if the caller passes an rvalue reference, that is foo(std::move(mystring)); because std::string provides a move constructor.
Using an rvalue here is a poor choice:
void foo(std::string&&)
because it places the burden of preparing the object on the caller. In particular if the caller wants to pass a copy of a string to this function, they have to do that explicitly;
std::string s;
foo(s); // XXX: doesn't compile
foo(std::string(s)); // have to create copy manually
And if you want to pass a mutable reference to a variable, just use a regular lvalue reference:
void foo(std::string&);
Using rvalue references in this case is technically possible, but semantically improper and totally confusing.
The only, only place where an rvalue reference makes sense is in a move constructor or move assignment operator. In any other situation pass-by-value or lvalue references are usually the right choice and avoid a lot of confusion.
Note: do not confuse rvalue references with forwarding references that look exactly the same but work totally differently, as in:
template <class T>
void foo(T&& t) {
}
In the above example t looks like a rvalue reference parameter, but is actually a forwarding reference (because of the template type), which is an entirely different can of worms.
This is a very minimal example:
class Foo
{
public:
Foo(int x) {};
};
void ProcessFoo(Foo& foo)
{
}
int main()
{
ProcessFoo(Foo(42));
return 0;
}
The above compiles fine on Visual Studio, but generates an error on Linux and Mac.
Compiling the above generates this:
$ g++ -std=c++11 -c newfile.cpp
newfile.cpp: In function ‘int main()’:
newfile.cpp:23:23: error: invalid initialization of non-const reference of type ‘Foo&’ from an rvalue of type ‘Foo’
ProcessFoo(Foo(42));
^
newfile.cpp:14:6: note: in passing argument 1 of ‘void ProcessFoo(Foo&)’
void ProcessFoo(Foo& foo)
I've found three workarounds:
Create a temp variable for the invocation of ProcessFoo.
Like this:
Foo foo42(42);
ProcessFoo(foo42);
ProcessFoo takes a const reference: void ProcessFoo(const Foo& foo)
ProcessFoo just lets Foo get passed by value. void ProcessFoo(Foo foo)
Why is the compiler forbidding my original code? (What is it guarding against)? What is it about each of the three workarounds above that satisfies the compiler? What would MSVC allow it, but not g++?
By design, C++ only allows a temporary to be passed to a const reference, value, or rvalue reference. The idea is that a function taking a non-const reference parameter is stating that it wants to modify the parameter and allowing it to go back to the caller. Doing so with a temporary is meaningless and most likely an error.
And I don't know what version of g++ you're running. It doesn't work here: http://coliru.stacked-crooked.com/a/43096cb398cbc973
Why is the compiler forbidding my original code?
Because it is forbidden by the Standard:
8.5.3 References 5
...
Otherwise, the reference shall be an lvalue reference to a non-volatile const type (i.e., cv1 shall be
const), or the reference shall be an rvalue reference.
[ Example:
double& rd2 = 2.0; // error: not an lvalue and reference not const
...
'
What is it guarding against?
Inadvertently modifying an object that is going to be destructed after the function call.
What is it about each of the three workarounds above that satisfies the compiler?
1 Creates a named object and 3 a copy.
2 Works because the lifetime of the object is simply extended, and changes to it are prevented at the same time.
What would MSVC allow it, but not g++?
Because it is a language extension. Disable it by going to Property Pages->C/C++->Language->Disable Language Extensions and you'll get an error.
Why is the compiler forbidding my original code?
MSVC has an extension that allows temporaries to bind to non-const lvalue-references. Of course this isn't a standard-conforming feature so I would stay away from it to be portable. For example, it doesn't work with the latest versions of GCC and Clang as you've seen.
What is it about each of the three workarounds above that satisfies the compiler?
Back in C++03, expressions could only be lvalues or rvalues. References could only designate the "lvalueness" of an object, and so it was used with the intention of aliasing a preexisting object. By contrast, rvalues don't exist beyond the expression in which they appear. Also, the end result of references was normally to copy or modify the object, and it doesn't make much sense to the language to modify an rvalue like 55 for example.
The rules allow you to bind an rvalue to a lvalue-reference to const, in which case the temporary's lifetime is extended to the lifetime of the reference. When you take an object by value the object is copied.
With C++11 we have rvalue-references and xvalues which were made for the purpose of exchanging ownership. With this is lessens the usefulness of lvalue-references to const. Moreover, taking by-value causes a move if it is an rvalue.
Once you declared the prototype for ProcessFoo as
void ProcessFoo(Foo& foo)
You are conveying your intent as the formal parameter "foo" is subject to modification as it is not being passed by const &.
At the call-site,
ProcessFoo(Foo(42));
Foo(42) is creating a temporary stack object that is not modifiable. It is okay to pass-by-value or pass-by-ref-to-const to a method.
As you listed yourself, satisfying those constraints, makes the compiler happy.
is giving you an object that is not compiler generated and is under your control.
Informs the compiler that the method guarantees const-ness of the object.
Informs the compiler that this (temporary) object is passed by value and hence no issues.
I am quite puzzled by the std::move stuff. Assume I have this
piece of code:
string foo() {
string t = "xxxx";
return t;
}
string s = foo();
How many times the string constructor is called? Is it 2 or 3?
Is the compiler going to use move for this line?
string s = foo();
If so, in the function I am not even returning rvalue reference, so how could the
compiler invoke the move constructor?
It depends on the compiler. In this case, the standard requires that there will be at least one constructor call. Namely, the construction of t.
But the standard allows the possibility of two others: the move-construction of the value output of foo from t, and the move-construction of s from the value output of foo. Most decent compilers will forgo these constructors by constructing t directly in the memory for s. This optimization is made possible because the standard allows these constructors to not be called if the compiler chooses not to.
This is called copy/move "elision".
If so, in the function I am not even returning rvalue reference, so how could the compiler invoke the move constructor?
You seem to be laboring under the misconception that && means "move", and that if there's no && somewhere, then movement can't happen. Or that move construction requires move, which also is not true.
C++ is specified in such a way that certain kinds of expressions in certain places are considered valid to move from. This means that the value or reference will attempt to bind to a && parameter before binding to a & parameter. Temporaries, for example, will preferentially bind to a && parameter before a const& one. That's why temporaries used to construct values of that type will be moved from.
If you have a function which returns a value of some type T, and a return expression is of the form return x, where x is a named variable of type T of automatic storage duration (ie: a function parameter or stack variable), then the standard requires that this return expression move construct the returned value from x.
The return value of foo is a temporary. The rules of C++ require that temporaries bind to && parameters before const&. So you get move construction into s.
For example:
void f(T&& t); // probably making a copy of t
void g()
{
T t;
// do something with t
f(std::move(t));
// probably something else not using "t"
}
Is void f(T const& t) equivalent in this case because any good compiler will produce the same code? I'm interested in >= VC10 and >= GCC 4.6 if this matters.
EDIT:
Based on the answers, I'd like to elaborate the question a bit:
Comparing rvalue-reference and pass-by-value approaches, it's so easy to forgot to use std::move in pass-by-value. Can compiler still check that no more changes are made to the variable and eliminate an unnecessary copy?
rvalue-reference approach makes only optimized version "implicit", e.g. f(T()), and requires the user to explicitly specify other cases, like f(std::move(t)) or to explicitly make a copy f(T(t)); if the user isn't done with t instance. So, in this optimization-concerned light, is rvalue-reference approach considered good?
It's definitely not the same. For once T && can only bind to rvalues, while T const & can bind both to rvalues and to lvalues. Second, T const & does not permit any move optimizations. If you "probably want to make a copy of t", then T && allows you to actually make a move-copy of t, which is potentially more efficient.
Example:
void foo(std::string const & s) { std::string local(s); /* ... */ }
int main()
{
std::string a("hello");
foo(a);
}
In this code, the string buffer containing "hello" must exist twice, once in the body of main, and another time in the body of foo. By contrast, if you used rvalue references and std::move(a), the very same string buffer can be "moved around" and only needs to be allocated and populated one single time.
As #Alon points out, the right idiom is in fact passing-by-value:
void foo(std::string local) { /* same as above */ }
int main()
{
std::string a("hello");
foo(std::move(a));
}
Well, it depends what f does with t, if it creates a copy of it, then I would even go at length of doing this:
void f(T t) // probably making a copy of t
{
m_newT = std::move(t); // save it to a member or take the resources if it is a c'tor..
}
void g()
{
T t;
// do something with t
f(std::move(t));
// probably something else not using "t"
}
Then you allow the move c'tors optimization to happen, you take 't' resources in any case, and if it was 'moved' to your function, then you even gain the non copy of moving it to the function, and if it was not moved then you probably had to have one copy
Now if at later on in the code you'd have:
f(T());
Then ta da, free move optimization without the f user even knowing..
Note that quote: "is void f(T const& t) equivalent in this case because any good compiler will produce the same code?"
It is not equivelent, it is LESS work, because only the "pointer" is transferred and no c'tors are called at all, neither move nor anything else
Taking an const lvalue reference and taking an rvalue reference are two different things.
Similarities:
Neither will cause an copy or move to take place because they are both references. A reference just references an object, it doesn't copy/move it in any way.
Differences:
A const lvalue reference will bind to anything (lvalue or rvalue). An rvalue reference will only bind to non-const rvalues - much more limited.
The parameter inside the function cannot be modified when it is a const lvalue reference. It can be modified when it's an rvalue reference (since it is non-const).
Let's look at some examples:
Taking const lvalue reference: void f(const T& t);
Passing an lvalue:
T t; f(t);
Here, t is an lvalue expression because it's the name of the object. A const lvalue reference can bind to anything, so t will happily be passed by reference. Nothing is copied, nothing is moved.
Passing an rvalue:
f(T());
Here, T() is an rvalue expression because it creates a temporary object. Again, a const lvalue reference can bind to anything, so this is okay. Nothing is copied, nothing is moved.
In both of these cases, the t inside the function is a reference to the object passed in. It can't be modified by the reference is const.
Taking an rvalue reference: `void f(T&& t);
Passing an lvalue:
T t;
f(t);
This will give you a compiler error. An rvalue reference will not bind to an lvalue.
Passing an rvalue:
f(T());
This will be fine because an rvalue reference can bind to an rvalue. The reference t inside the function will refer to the temporary object created by T().
Now let's consider std::move. First things first: std::move doesn't actually move anything. The idea is that you give it an lvalue and it turns it into an rvalue. That's all it does. So now, if your f takes an rvalue reference, you could do:
T t;
f(std::move(t));
This works because, although t is an lvalue, std::move(t) is an rvalue. Now the rvalue reference can bind to it.
So why would you ever take an rvalue reference argument? In fact, you shouldn't need to do it very often, except for defining move constructors and assignment operators. Whenever you define a function that takes an rvalue reference, you almost certainly want to give a const lvalue reference overload. They should almost always come in pairs:
void f(const T&);
void f(T&&);
Why is this pair of functions useful? Well, the first will be called whenever you give it an lvalue (or a const rvalue) and the second will be called whenever you give it a modifiable rvalue. Receiving an rvalue usually means that you've been given a temporary object, which is great news because that means you can ravage its insides and perform optimizations based on the fact that you know it's not going to exist for much longer.
So having this pair of functions allows you to make an optimization when you know you're getting a temporary object.
There's a very common example of this pair of functions: the copy and move constructors. They are usually defined like so:
T::T(const T&); // Copy constructor
T::T(T&&); // Move constructor
So a move constructor is really just a copy constructor that is optimized for when receiving a temporary object.
Of course, the object being passed isn't always a temporary object. As we've shown above, you can use std::move to turn an lvalue into an rvalue. Then it appears to be a temporary object to the function. Using std::move basically says "I allow you to treat this object as a temporary object." Whether it actually gets moved from or not is irrelevant.
However, beyond writing copy constructors and move constructors, you'd better have a good reason for using this pair of functions. If you're writing a function that takes an object and will behave exactly the same with it regardless of whether its a temporary object or not, simply take that object by value! Consider:
void f(T t);
T t;
f(t);
f(T());
In the first call to f, we are passing an lvalue. That will be copied into the function. In the second call to f, we are passing an rvalue. That object will be moved into the function. See - we didn't even need to use rvalue references to cause the object to be moved efficiently. We just took it by value! Why? Because the constructor that is used to make the copy/move is chosen based on whether the expression is an lvalue or an rvalue. Just let the copy/move constructors do their job.
As to whether different argument types result in the same code - well that's a different question entirely. The compiler operates under the as-if rule. This simply means that as long as the program behaves as the standard dictates, the compiler can emit whatever code it likes. So the functions may emit the same code if they happen to do exactly the same thing. Or they may not. However, it's a bad sign if you're functions that take a const lvalue reference and an rvalue reference are doing the same thing.