Stumbled upon few articles claiming that passing by value could improve performance if function is gonna make a copy anyway.
I never really thought about how pass-by-value might be implemented under the hood. Exactly what happens on stack when you do smth like this: F v = f(g(h()))?
After pondering a bit I came to conclusion that I'd implement it in such way that value returned by g() is created in locations where f() expects it to be. So, basically, no copy/move constructor calls -- f() will simply take ownership of object returned by g() and destroy it when execution leaves f()'s scope. Same for g() -- it'll take ownership of object returned by h() and destroy it on return.
Alas, compilers seem to disagree. Here is the test code:
#include <cstdio>
using std::printf;
struct H
{
H() { printf("H ctor\n"); }
~H() { printf("H dtor\n"); }
H(H const&) {}
// H(H&&) {}
// H(H const&) = default;
// H(H&&) = default;
};
H h() { return H(); }
struct G
{
G() { printf("G ctor\n"); }
~G() { printf("G dtor\n"); }
G(G const&) {}
// G(G&&) {}
// G(G const&) = default;
// G(G&&) = default;
};
G g(H) { return G(); }
struct F
{
F() { printf("F ctor\n"); }
~F() { printf("F dtor\n"); }
};
F f(G) { return F(); }
int main()
{
F v = f(g(h()));
return 0;
}
On MSVC 2015 it's output is exactly what I expected:
H ctor
G ctor
H dtor
F ctor
G dtor
F dtor
But if you comment out copy constructors it looks like this:
H ctor
G ctor
H dtor
F ctor
G dtor
G dtor
H dtor
F dtor
I suspect that removing user-provided copy constructor causes compiler to generate move-constructor, which in turn causes unnecessary 'move' which doesn't go away no matter how big objects in question are (try adding 1MB array as member variable). I.e. compiler prefers 'move' so much that it chooses it over not doing anything at all.
It seems like a bug in MSVC, but I would really like someone to explain (and/or justify) what is going on here. This is question #1.
Now, if you try GCC 5.4.0 output simply doesn't make any sense:
H ctor
G ctor
F ctor
G dtor
H dtor
F dtor
H has to be destroyed before F is created! H is local to g()'s scope! Note that playing with constructors has zero effect on GCC here.
Same as with MSVC -- looks like a bug to me, but can someone explain/justify what is going on here? That is question #2.
It is really silly that after many years of working with C++ professionally I run into issues like this... After almost 4 decades compilers still can't agree on how to pass values around?
For passing a parameter by value, the parameter is a local variable to the function, and it's initialized from the corresponding argument to the function call.
When returning by value, there is a value called the return value. This is initialized by the "argument" to the return expression. Its lifetime is until the end of the full-expression containing the function call.
Also there is an optimization called copy elision which can apply in a few cases. Two of those cases apply to returning by value:
If the return value is initialized by another object of the same type, then the same memory location can be used for both objects, and the copy/move step skipped (there are some conditions on exactly when this is allowed or disallowed)
If the calling code uses the return value to initialize an object of the same type, then the same memory location can be used for both the return value and the destination object, and the copy/move step is skipped. (Here the "object of the same type" includes function parameters).
It is possible for both of these to apply simultaneously. Also, as of C++14, copy elision is optional for the compiler.
In your call f(g(h())), here is the list of objects (without copy elision):
H default-constructed by return H();
H, the return value of h(), is copy-constructed from (step 1).
~H (step 1)
H, the parameter of g, is copy-constructed from (step 2).
G default-constructed by return G();
G, the return value of g(), is copy-constructed from (step 5).
~G (step 5)
~H (step 4) (see below)
G, the parameter of f, is copy-constructed from (step 6).
F default-constructed by return F();
F, the return value of f(), is move-constructed from (step 10).
~F (step 10)
~G (step 9) (see below)
F v is move-constructed from (step 11).
~F, ~G, ~H (steps 2, 6, 11) are destroyed - I think there is no required ordering of the three
~F(step 14)
For copy elision, steps 1+2+3 can be combined into "Return value of h() is default-constructed". Similarly for 5+6+7 and 10+11+12. However it is also possible to combine either 2+4 on their own into "Parameter of g is copy-constructed from 1", and also possible for both of these elisions to apply simultaneously , giving "Parameter of g is default-constructed".
Because copy elision is optional you may see different results from different compilers. It doesn't mean there is a compiler bug. You'll be glad to hear that in C++17 some copy elision scenarios are being made mandatory.
Your output in the second MSVC case would be more instructive if you included output text for the move-constructor. I would guess that in the first MSVC case it performed both simultaneous elisions that I mentioned above, whereas the second case omits the "2+4" and "6+9" elisions.
below: gcc and clang delay destruction of function parameters until the end of the full-expression that enclosed the function call. This is why your gcc output differs from MSVC.
As of the C++17 drafting process, it is implementation-defined whether these destructions occur where I had them in my list, or at the end of the full-expression. It could be argued that it was insufficiently specified in the earlier published standards. See here for further discussion.
This behavior is because of an optimization technique called copy elision. In a nutshell all of outputs you mentioned are valid! Yep! Because this technique is (the only one) allowed to modify the behavior of the program. More information can be found at What are copy elision and return value optimization?
Both M.M's and Ahmad's answers were sending me in right direction, but they both weren't fully correct. So I opted to write down a proper answer below...
function call and return in C++ has following semantic:
value passed as function argument gets copied into function scope and function gets invoked
return value gets copied into caller's scope, gets destroyed (when we reach end of return full expression) and execution leaves function scope
When it comes to implementing this on IA-32-like architecture it becomes painfully obvious that these copies are not required -- it is trivial to allocate uninitialized space on stack (for return value) and define function calling conventions in such way that it knows where to construct return value.
Same for argument passing -- if we pass rvalue as function argument, compiler can direct creation of that rvalue in such way that it will be created right were (subsequently called) function expects it to be.
I imagine this is main reason why copy elision was introduced to standard (and is made mandatory in C++17).
I am familiar with copy elision in general and read this page before. Unfortunately I missed two things:
the fact that this also applies to initialization of function arguments with rvalue (C++11 12.8.p32):
when a temporary class object that has not been bound to a reference
(12.2) would be copied/moved to a class object with the same
cv-unqualified type, the copy/move operation can be omitted by
constructing the temporary object directly into the target of the
omitted copy/move
when copy elision kicks in it affects object lifetime in a very peculiar way:
When certain criteria are met, an implementation is allowed to omit
the copy/move construction of a class object, even if the copy/move
constructor and/or destructor for the object have side effects. In
such cases, the implementation treats the source and target of the
omitted copy/move operation as simply two different ways of referring
to the same object, and the destruction of that object occurs at the
later of the times when the two objects would have been destroyed
without the optimization. This elision of copy/move operations, called
copy elision, is permitted in the following circumstances (which may
be combined to eliminate multiple copies)
This explains GCC output -- we pass some rvalue into a function, copy elision kicks in and we end up with one object being referred via two different ways and lifetime = longest of all of them (which is a lifetime of temporary in our F v = ...; expression). So, basically, GCC output is completely standard compliant.
Now, this also means that MSVC is not standard compliant! It successfully applied both copy elisions, but resulting object lifetime is too short.
Second MSVC output conforms the standard -- it applied RVO, but decided to not apply copy elision for function parameter. I still think it is a bug in MSVC, even though code is ok from standard point of view.
Thank you both M.M and Ahmad for pushing me in right direction.
Now little rant about lifetime rule enforced by standard -- I think it was meant to be used only with RVO.
Alas it doesn't make a lot of sense when applied to eliding copy of function argument. In fact, combined with C++17 mandatory copy elision rule it permits crazy code like this:
T bar();
T* foo(T a) { return &a; }
auto v = foo(bar())->my_method();
this rule forces T to be destroyed only at the end of full expression. This code will become correct in C++17. It is ugly and should not be allowed in my opinion. Plus, you'll end up destroying these objects on caller side (instead of inside of a function) -- needlessly increasing code size and complicating process of figuring out if given function is a nothrow or not.
In other words, I personally prefer MSVC output #1 (as most 'natural'). Both MSVC output #2 and GCC output should be banned. I wonder if this idea can be sold to C++ standardization committee...
edit: apparently in C++17 lifetime of temporary will become 'unspecified' thus allowing MSVC's behavior. Yet another unnecessary dark corner in the language. They should have simply mandated MSVC's behavior.
Related
I've read several posts about temporary object's lifetime. And in a word I learn that:
the temporary is destroyed after the end of the full-expression
containing
it.
But this code is out of my expectation:
#include <memory>
#include <iostream>
void fun(std::shared_ptr<int> sp)
{
std::cout << "fun: sp.use_count() == " << sp.use_count() << '\n';
//I expect to get 2 not 1
}
int main()
{
fun(std::make_shared<int>(5));
}
So I think I have 2 smart pointer objects here, one is std::make_shared<int>(5), the temporary unnamed object and the other sp which is a local variable inside the function. So based on my understanding, the temporary one won't "die" before completing the function call. I expect output to be 2 not 1. What's wrong here?
Pre-C++17, sp is move-constructed from the temporary if the move is not elided to begin with. In either case, sp is the sole owner of the resource, so the use count is rightly reported as 1. This is overload 10)† in this reference.
While the temporary still exists, if not elided, it is in a moved-from state and no longer holds any resource, so it doesn't contribute to the resource's use count.
Since C++17, no temporary is created thanks to guaranteed copy/move elision, and sp is constructed in place.
† Exact wording from said reference:
10) Move-constructs a shared_ptr from r. After the construction, *this contains a copy of the previous state of r, r is empty and its stored pointer is null. [...]
In our case, r refers to the temporary and *this to sp.
c++ has a strange concept known as elision.
Elision is a process whereby the compiler is allowed to take the lifetime of two objects and merge them. Typically people say that the copy or move constructor "is elided", but what is really elided is the identity of two seemingly distinct objects.
As a rule of thumb, when an anonymous temporary object is used to directly construct another object, their lifetimes can be elided together. So:
A a = A{}; // A{} is elided with a
void f(A);
f(A{}); // temporary A{} is elided with argument of f
A g();
f(g()); // return value of g is elided with argument of f
There are also situations where named variables can be elided with return values, and more than two objects can be elided together:
A g() {
A a;
return a; // a is elided with return value of g
}
A f() {
A x = g(); // x is elided with return value of g
// which is elided with a within g
return x; // and is then elided with return value of f
}
A bob = f(); // and then elided with bob.
Only one instance of A exists in the above code; it just has many names.
In c++17 things go even further. Prior to that the objects in question had to logically be copyable/movable, and elision simply eliminated calls the the constructor and shared the objects identity.
After c++17 some things that used to be elision are (in some sense) "guaranteed elision", which is really a different thing. "Guaranteed elision" is basically the idea that prvalues (things that used to be temporaries in pre-c++17) are now abstract instructions on how to create an object.
In certain circumstances temporaries are instantiated from them, but in others they are just used to construct some other object in some other spot.
So in c++17 you should think of this function:
A f();
as a function that returns instructions on how to create a A. When you do this:
A a = f();
you are saying "use the instructions that f returns to construct an A named a".
Similarly, A{} is no longer a temporary but instructions no how to create an A. If you put it on a line by itself those instructions are used to create a temporary, but in most contexts no temporary logically or actually exists.
template<class T, class...Us>
std::shared_ptr<T> make_shared(Us&&...);
this is a function that returns instructions on how to create a shared_ptr<T>.
fun(std::make_shared<int>(5));
here you apply these instructions to the agument of fun, which is of type std::shared_ptr<int>.
In pre-[C++17] without hostile compiler flags, the result with elision is practically the same here. In that case, the temporaries identity is merged with the argument of fun.
In no practical case will there be a temporary shared_ptr with a reference count of 0; other answers which claim this are wrong. The one way where that can occur is if you pass in flags that your compiler from performing elision (the above hostile compiler flags).
If you do pass in such flags, the shared_ptr is moved-from into the argument of fun, and it exists with a reference count of 0. So use_count will remain 0.
In addition to the move construction of std::shared_ptr, there is another aspect to consider: in-place creation of function argument passed by value. This is an optimization that compilers usually do. Consider the exemplary type
struct A {
A() { std::cout << "ctor\n"; }
A(const A&) { std::cout << "copy ctor\n"; }
};
together with a function that takes an instance of A by value
void f(A) {}
When the function parameter is passed as an rvalue like this
f(A{});
the copy constructor won't be called unless you explicitly compile with -fno-elide-constructors. In C++17, you can even delete the copy constructor
A(const A&) = delete;
because the copy elision is guaranteed. With this in mind: the temporary object that you pass as a function argument is "destroyed after the end of the full-expression containing it" only if there is a temporary, and a code snippet might suggest the existence of one even though it's easily (and since C++17: guaranteed to be) optimized out.
Yet another "why must std::move prevent the (unnamed) return-value-optimization?" question:
Why does std::move prevent RVO? explains that the standard specifically requires that the function's declared return type must match the type of the expression in the return statement. That explains the behavior of conforming compilers; however, it does not explain the rationale for the restriction.
Why do the rules for RVO not make an exception for the case where the function's return type is T and the type of the return expression is T&&?
I also am aware that implementing such things in compilers doesn't come for free. I am suggesting only that such an exception be allowed but not required.
I also am aware that return std::move(...) is unnecessary since C++11 already requires that move semantics be used when the RVO can't be applied. Nevertheless, why not tolerate an explicit request for optimization instead of turning it into a pessimization?
(Aside: Why are the return-value-optimization and rvo tags not synonyms?)
auto foo() -> T&&;
auto test() -> T
{
return foo();
}
You say in this case the RVO should be allowed to be applied. However consider this legal implementation of foo:
T val;
auto foo() -> T&&
{
return static_cast<T&&>(val); // because yes, it's legal
}
The moral: only with prvalues you know for sure you have a temporary and most important you know the exact lifetime of the temporary so you can elide its construction and destruction. But with xvalues (e.g. T&& return) you don't know if that is indeed bound to a temporary, you don't know when that value was created and when it goes out of scope or even if you know you can't change it's construction and destruction point like the above example.
I'm not sure that I fully understand. If RVO were allowed to be
applied to test(), why would that be worse than if test did: T temp = foo(); return temp; which would allow NRVO?
It's not that it is worse. It is just not possible. With your example temp is a local variable in the function where you want to apply NRVO i.e. test. As such it is an object fully "known" in the context of test, its lifetime is known, the normal point of ctor and dtor is known. So instead of creating the temp variable in the stack frame of test it is created in the stack frame of the caller. This means there is no copy of the object from the stack frame of test to the stack frame of the caller. Also please see that in this example foo() is completely irrelevant. It could have been anything in the initialization of temp:
auto test() -> T
{
T temp = /*whatever*/;
return temp; // NRVO allowed
}
But with return foo() you can't elide the copy simply because you can't know to what object the return reference binds to. It can be a reference to any object.
unique_ptr<T> does not allow copy construction, instead it supports move semantics. Yet, I can return a unique_ptr<T> from a function and assign the returned value to a variable.
#include <iostream>
#include <memory>
using namespace std;
unique_ptr<int> foo()
{
unique_ptr<int> p( new int(10) );
return p; // 1
//return move( p ); // 2
}
int main()
{
unique_ptr<int> p = foo();
cout << *p << endl;
return 0;
}
The code above compiles and works as intended. So how is it that line 1 doesn't invoke the copy constructor and result in compiler errors? If I had to use line 2 instead it'd make sense (using line 2 works as well, but we're not required to do so).
I know C++0x allows this exception to unique_ptr since the return value is a temporary object that will be destroyed as soon as the function exits, thus guaranteeing the uniqueness of the returned pointer. I'm curious about how this is implemented, is it special cased in the compiler or is there some other clause in the language specification that this exploits?
is there some other clause in the language specification that this exploits?
Yes, see 12.8 §34 and §35:
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object [...]
This elision of copy/move operations, called copy elision, is permitted [...]
in a return statement in a function with a class return type, when the expression is the name of
a non-volatile automatic object with the same cv-unqualified type as the function return type [...]
When the criteria for elision of a copy operation are met and the object to be copied is designated by an lvalue,
overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue.
Just wanted to add one more point that returning by value should be the default choice here because a named value in the return statement in the worst case, i.e. without elisions in C++11, C++14 and C++17 is treated as an rvalue. So for example the following function compiles with the -fno-elide-constructors flag
std::unique_ptr<int> get_unique() {
auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
return ptr; // <- 2, moved into the to be returned unique_ptr
}
...
auto int_uptr = get_unique(); // <- 3
With the flag set on compilation there are two moves (1 and 2) happening in this function and then one move later on (3).
This is in no way specific to std::unique_ptr, but applies to any class that is movable. It's guaranteed by the language rules since you are returning by value. The compiler tries to elide copies, invokes a move constructor if it can't remove copies, calls a copy constructor if it can't move, and fails to compile if it can't copy.
If you had a function that accepts std::unique_ptr as an argument you wouldn't be able to pass p to it. You would have to explicitly invoke move constructor, but in this case you shouldn't use variable p after the call to bar().
void bar(std::unique_ptr<int> p)
{
// ...
}
int main()
{
unique_ptr<int> p = foo();
bar(p); // error, can't implicitly invoke move constructor on lvalue
bar(std::move(p)); // OK but don't use p afterwards
return 0;
}
unique_ptr doesn't have the traditional copy constructor. Instead it has a "move constructor" that uses rvalue references:
unique_ptr::unique_ptr(unique_ptr && src);
An rvalue reference (the double ampersand) will only bind to an rvalue. That's why you get an error when you try to pass an lvalue unique_ptr to a function. On the other hand, a value that is returned from a function is treated as an rvalue, so the move constructor is called automatically.
By the way, this will work correctly:
bar(unique_ptr<int>(new int(44));
The temporary unique_ptr here is an rvalue.
I think it's perfectly explained in item 25 of Scott Meyers' Effective Modern C++. Here's an excerpt:
The part of the Standard blessing the RVO goes on to say that if the conditions for the RVO are met, but compilers choose not to perform copy elision, the object being returned must be treated as an rvalue. In effect, the Standard requires that when the RVO is permitted, either copy elision takes place or std::move is implicitly applied to local objects being returned.
Here, RVO refers to return value optimization, and if the conditions for the RVO are met means returning the local object declared inside the function that you would expect to do the RVO, which is also nicely explained in item 25 of his book by referring to the standard (here the local object includes the temporary objects created by the return statement). The biggest take away from the excerpt is either copy elision takes place or std::move is implicitly applied to local objects being returned. Scott mentions in item 25 that std::move is implicitly applied when the compiler choose not to elide the copy and the programmer should not explicitly do so.
In your case, the code is clearly a candidate for RVO as it returns the local object p and the type of p is the same as the return type, which results in copy elision. And if the compiler chooses not to elide the copy, for whatever reason, std::move would've kicked in to line 1.
One thing that i didn't see in other answers is To clarify another answers that there is a difference between returning std::unique_ptr that has been created within a function, and one that has been given to that function.
The example could be like this:
class Test
{int i;};
std::unique_ptr<Test> foo1()
{
std::unique_ptr<Test> res(new Test);
return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
// return t; // this will produce an error!
return std::move(t);
}
//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));
I would like to mention one case where you must use std::move() otherwise it will give an error.
Case: If the return type of the function differs from the type of the local variable.
class Base { ... };
class Derived : public Base { ... };
...
std::unique_ptr<Base> Foo() {
std::unique_ptr<Derived> derived(new Derived());
return std::move(derived); //std::move() must
}
Reference: https://www.chromium.org/developers/smart-pointer-guidelines
I know it's an old question, but I think an important and clear reference is missing here.
From https://en.cppreference.com/w/cpp/language/copy_elision :
(Since C++11) In a return statement or a throw-expression, if the compiler cannot perform copy elision but the conditions for copy elision are met or would be met, except that the source is a function parameter, the compiler will attempt to use the move constructor even if the object is designated by an lvalue; see return statement for details.
We have the following type X and function f:
struct X { ... };
X f() { ... };
Now consider three alternative definitions of another function g:
(1)
void g()
{
X x = f();
...
}
(2)
void g()
{
X& x = f();
...
}
(3)
void g()
{
X&& x = f();
...
}
What is the difference in defined behavior (or potential behaviour) between the three different cases? (assume the placeholdered '...' code is identical in the three cases)
Update:
What if g returned an X: is the following legal and correct?
X g()
{
X&& x = f();
...
return move(x);
}
(Is the move necessary, does it do anything?)
Would you expect RVO to chain so the below would produce the same code?
X g()
{
X x = f();
...
return x;
}
2 is illegal. The other two are essentially identical- x is a mutable lvalue of type X whose life ends when g() returns.
Of course, strictly, the first calls the move constructor (but is a prime candidate for some RVO/NRVO action) and the third does not, so if X is immovable (very odd...) the third case is legal but the first is not, and the first may be more expensive. However, the reality of compiler opts and immovable types is that this is almost entirely a technicality and I'd be surprised if you could actually demonstrate any such case.
The expression f() returns by value, so it's a prvalue.
this creates a new object of type X initialized with the expression f() which is a prvalue of type X. How the new X object will be constructed depends on whether it has a move constructor or not. If it has a move constructor (or a template constructor that accepts X rvalues) it will be called, otherwise if it has a copy constructor that will be called. In practice the compiler will almost certainly elide the constructor call, but the appropriate constructor must be accessible and not deleted.
This doesn't even compile, you get a downvote for not trying it before posting!
This create a new reference of type X&& and initializes it by binding it to the prvalue returned by f(). That prvalue will have its lifetime extended to the same lifetime as the reference x.
The difference in behaviour is probably nothing, assuming the move/copy is elided, but there is a difference in semantics between (1) and (3) because one does overload resolution for a constructor, which could fail, and the other always works.
What if g returned an X: is the following legal and correct?
It's legal. The move is unnecessary, there's a special rule that says when returning a local variable by value the constructor lookup is first done as though the variable was an rvalue so if X has a move constructor it will be used, whether or not you use move(x). RVO should "chain". If g returned X& you'd have a problem in both cases, because the object it would bind to would go out of scope at the end of g.
(N.B. It's good practice to always qualify std::move to prevent ADL. Similarly for std::forward. If you mean to call std::move or std::forward then be explicit, don't rely on there being no overloads in scope or visible, move and forward are not customization points like swap.)
Instead of learning C++ by asking questions on SO, why not write code to test what happens and prove it to yourself? With G++ you can use the -fno-elide-constructors flag to turn off constructor elision to see what would happen in the absence of elision, and when not using that flag (the default) you can easily test whether RVO "chains" for yourself.
unique_ptr<T> does not allow copy construction, instead it supports move semantics. Yet, I can return a unique_ptr<T> from a function and assign the returned value to a variable.
#include <iostream>
#include <memory>
using namespace std;
unique_ptr<int> foo()
{
unique_ptr<int> p( new int(10) );
return p; // 1
//return move( p ); // 2
}
int main()
{
unique_ptr<int> p = foo();
cout << *p << endl;
return 0;
}
The code above compiles and works as intended. So how is it that line 1 doesn't invoke the copy constructor and result in compiler errors? If I had to use line 2 instead it'd make sense (using line 2 works as well, but we're not required to do so).
I know C++0x allows this exception to unique_ptr since the return value is a temporary object that will be destroyed as soon as the function exits, thus guaranteeing the uniqueness of the returned pointer. I'm curious about how this is implemented, is it special cased in the compiler or is there some other clause in the language specification that this exploits?
is there some other clause in the language specification that this exploits?
Yes, see 12.8 §34 and §35:
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object [...]
This elision of copy/move operations, called copy elision, is permitted [...]
in a return statement in a function with a class return type, when the expression is the name of
a non-volatile automatic object with the same cv-unqualified type as the function return type [...]
When the criteria for elision of a copy operation are met and the object to be copied is designated by an lvalue,
overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue.
Just wanted to add one more point that returning by value should be the default choice here because a named value in the return statement in the worst case, i.e. without elisions in C++11, C++14 and C++17 is treated as an rvalue. So for example the following function compiles with the -fno-elide-constructors flag
std::unique_ptr<int> get_unique() {
auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
return ptr; // <- 2, moved into the to be returned unique_ptr
}
...
auto int_uptr = get_unique(); // <- 3
With the flag set on compilation there are two moves (1 and 2) happening in this function and then one move later on (3).
This is in no way specific to std::unique_ptr, but applies to any class that is movable. It's guaranteed by the language rules since you are returning by value. The compiler tries to elide copies, invokes a move constructor if it can't remove copies, calls a copy constructor if it can't move, and fails to compile if it can't copy.
If you had a function that accepts std::unique_ptr as an argument you wouldn't be able to pass p to it. You would have to explicitly invoke move constructor, but in this case you shouldn't use variable p after the call to bar().
void bar(std::unique_ptr<int> p)
{
// ...
}
int main()
{
unique_ptr<int> p = foo();
bar(p); // error, can't implicitly invoke move constructor on lvalue
bar(std::move(p)); // OK but don't use p afterwards
return 0;
}
unique_ptr doesn't have the traditional copy constructor. Instead it has a "move constructor" that uses rvalue references:
unique_ptr::unique_ptr(unique_ptr && src);
An rvalue reference (the double ampersand) will only bind to an rvalue. That's why you get an error when you try to pass an lvalue unique_ptr to a function. On the other hand, a value that is returned from a function is treated as an rvalue, so the move constructor is called automatically.
By the way, this will work correctly:
bar(unique_ptr<int>(new int(44));
The temporary unique_ptr here is an rvalue.
I think it's perfectly explained in item 25 of Scott Meyers' Effective Modern C++. Here's an excerpt:
The part of the Standard blessing the RVO goes on to say that if the conditions for the RVO are met, but compilers choose not to perform copy elision, the object being returned must be treated as an rvalue. In effect, the Standard requires that when the RVO is permitted, either copy elision takes place or std::move is implicitly applied to local objects being returned.
Here, RVO refers to return value optimization, and if the conditions for the RVO are met means returning the local object declared inside the function that you would expect to do the RVO, which is also nicely explained in item 25 of his book by referring to the standard (here the local object includes the temporary objects created by the return statement). The biggest take away from the excerpt is either copy elision takes place or std::move is implicitly applied to local objects being returned. Scott mentions in item 25 that std::move is implicitly applied when the compiler choose not to elide the copy and the programmer should not explicitly do so.
In your case, the code is clearly a candidate for RVO as it returns the local object p and the type of p is the same as the return type, which results in copy elision. And if the compiler chooses not to elide the copy, for whatever reason, std::move would've kicked in to line 1.
One thing that i didn't see in other answers is To clarify another answers that there is a difference between returning std::unique_ptr that has been created within a function, and one that has been given to that function.
The example could be like this:
class Test
{int i;};
std::unique_ptr<Test> foo1()
{
std::unique_ptr<Test> res(new Test);
return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
// return t; // this will produce an error!
return std::move(t);
}
//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));
I would like to mention one case where you must use std::move() otherwise it will give an error.
Case: If the return type of the function differs from the type of the local variable.
class Base { ... };
class Derived : public Base { ... };
...
std::unique_ptr<Base> Foo() {
std::unique_ptr<Derived> derived(new Derived());
return std::move(derived); //std::move() must
}
Reference: https://www.chromium.org/developers/smart-pointer-guidelines
I know it's an old question, but I think an important and clear reference is missing here.
From https://en.cppreference.com/w/cpp/language/copy_elision :
(Since C++11) In a return statement or a throw-expression, if the compiler cannot perform copy elision but the conditions for copy elision are met or would be met, except that the source is a function parameter, the compiler will attempt to use the move constructor even if the object is designated by an lvalue; see return statement for details.