Why can you return a std::unique_ptr without std::move?
Asked Answered
W

7

501

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?

Whirlwind answered 30/11, 2010 at 17:44 Comment(3)
Hypothetically, if you were implementing a factory method, would you prefer 1 or 2 to return the factory's output? I presume that this would be the most common use of 1 because, with a proper factory, you actually want the ownership of the constructed thing to pass to the caller.Junno
@Junno ? They both pass ownership of the unique_ptr. The whole question is about 1 and 2 being two different ways of achieving the same thing.Whirlwind
in this case, the RVO takes place in c++0x as well, the destruction of the unique_ptr object will be once which is performed after main function exits , but not when the foo exits.Enkindle
F
287

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

Frippery answered 30/11, 2010 at 18:6 Comment(10)
@juanchopanza Do you essentially mean that foo() is indeed also about to be destroyed (if it were not assigned to anything), just like the return value within the function, and hence it makes sense that C++ uses a move constructor when doing unique_ptr<int> p = foo();?Brachial
This answer says an implementation is allowed to do something... it doesn't say it must, so if this was the only relevant section, that would imply relying on this behavior isn't portable. But I don't think that's right. I am inclined to think the correct answer has more to do with the move constructor, as described in Nikola Smiljanic's and Bartosz Milewski's answer.Fixity
@DonHatch It says it's "allowed" to perform copy/move elision in those cases, but we're not talking about copy elision here. It's the second quoted paragraph that applies here, which piggy-backs on the copy elision rules, but is not copy elision itself. There is no uncertainty in the second paragraph - it's totally portable.Pernik
@juanchopanza I realise this is now 2 years later, but do you still feel that this is wrong? As I mentioned in the previous comment, this isn't about copy elision. It just so happens that in the cases where copy elision might apply (even if it can't apply with std::unique_ptr), there is a special rule to first treat objects as rvalues. I think this agrees entirely with what Nikola has answered.Pernik
@JosephMansfield No, I am wrong! I misread the quote. I will my strangely up-voted comment :-)Listel
@JosephMansfield right you are. Removing my downvote. [Oh ffs, my vote is locked in and can't be changed unless the answer gets edited!?]Fixity
This copy elision is known as RVO or Return Value Optiomization. This exists long before C++11. See en.wikipedia.org/wiki/Copy_elision#Return_value_optimizationFeet
So why do I still get the error "attempting to reference a deleted function" for my move-only type (removed copy constructor) when returning it exactly in the same way as this example?Feet
I found the issue finally, it was caused by a member not being moveable ;-)Feet
Where can we find worst case ... is treated as an rvalue. in the std reference? Thx!Dissipate
A
142

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;
}
Agni answered 30/11, 2010 at 17:48 Comment(6)
@Fred - well, not really. Although p is not a temporary, the result of foo(), what's being returned, is; thus it's an rvalue and can be moved, which makes the assignment in main possible. I'd say you were wrong except that Nikola then seems to apply this rule to p itself which IS in error.Tribal
Exactly what I wanted to say, but couldn't find the words. I've removed that part of the answer since it wasn't very clear.Wellchosen
I have a question: in the original question, is there any substantial difference between Line 1 and Line 2? In my view it's the same since when constructing p in main, it only cares about the type of return type of foo, right?Available
@HongxuChen In that example there's absolutely no difference, see the quote from the standard in the accepted answer.Wellchosen
Actually, you can use p afterwards, as long as you assign to it. Until then, you can't try to reference the contents.Acquittance
Isn't the copy constructor for std::unique_ptr removed to be move-only even RVO still needs it?Feet
E
47

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.

Ebracteate answered 30/11, 2010 at 23:0 Comment(3)
I think the point is more, why can p - "obviously" an lvalue - be treated as an rvalue in the return statement return p; in the definition of foo. I don't think there's any issue with the fact that the return value of the function itself can be "moved".Velum
Does wrapping the returned value from the function in std::move mean that it will be moved twice?Defer
@RodrigoSalazar std::move is just a fancy cast from a lvalue reference (&) to an rvalue reference (&&). Extraneous usage of std::move on an rvalue reference will simply be a noopErogenous
S
27

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.

Selector answered 4/10, 2017 at 18:38 Comment(1)
Is it RVO though? In The C++ Programming Language 4th Ed, p.113 shows the line return unique_ptr<X>{ new X{i} }; which I believe is pure move semantics, not RVO. If this line were changed to the following 2 lines, does that change things to be RVO? unique_ptr<X> mypX{ new X{i} }; return mypX;Destine
A
11

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));
Abert answered 3/7, 2017 at 16:6 Comment(5)
It is mentioned in the answer by fredoverflow - clearly highlighted "automatic object". A reference (including an rvalue reference) is not an automatic object.Ungrudging
@TobySpeight Ok, sorry. I guess my code is just a clarification then.Abert
Thanks for this answer! I've been trying to debug a problem caused by this for days now, and reading this answer made me realize what was wrong.Junna
May I ask why std::move() is necessary in foo2 ?Canterbury
@Canterbury unique_ptr cannot be copied, only moved.Abert
C
11

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

Cabe answered 22/6, 2020 at 12:42 Comment(2)
But if I remove std::move, there is no error when compiling. The compiler will try calling the move constructor if copy constructor is not callable. So the std::move here may be redundant.Marlomarlon
It does not seem like the document you referenced says std::move() should be used in this case. I think it is the opposite.Blubberhead
E
4

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.

Entomology answered 4/10, 2022 at 10:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.