405

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?

Praetorian
  • 100,267
  • 15
  • 224
  • 307
  • 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. – Xharlie Sep 15 '15 at 11:10
  • 7
    @Xharlie ? 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. – Praetorian Sep 15 '15 at 16:28
  • 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. – ampawd Jan 19 '19 at 14:17

6 Answers6

242

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

Curious
  • 19,352
  • 6
  • 45
  • 114
fredoverflow
  • 237,063
  • 85
  • 359
  • 638
  • @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 p = foo();`? – 7cows Jun 04 '13 at 16:10
  • 1
    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. – Don Hatch Jul 22 '14 at 22:19
  • 6
    @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. – Joseph Mansfield Sep 24 '14 at 08:34
  • @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. – Joseph Mansfield Sep 24 '14 at 08:36
  • @JosephMansfield No, I am wrong! I misread the quote. I will my strangely up-voted comment :-) – juanchopanza Sep 24 '14 at 08:59
  • @JosephMansfield right you are. Removing my downvote. [Oh ffs, my vote is locked in and can't be changed unless the answer gets edited!?] – Don Hatch Sep 24 '14 at 22:17
  • This copy elision is known as RVO or Return Value Optiomization. This exists long before C++11. See https://en.wikipedia.org/wiki/Copy_elision#Return_value_optimization – DrumM Sep 13 '18 at 12:00
  • 1
    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? – DrumM May 06 '19 at 07:58
  • I found the issue finally, it was caused by a member not being moveable ;-) – DrumM May 06 '19 at 09:23
  • Where can we find `worst case ... is treated as an rvalue.` in the std reference? Thx! – Gabriel Feb 20 '20 at 13:38
114

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;
}
Azeem
  • 7,094
  • 4
  • 19
  • 32
Nikola Smiljanić
  • 26,180
  • 6
  • 45
  • 59
  • 3
    @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. – Edward Strange Nov 30 '10 at 18:39
  • 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. – Nikola Smiljanić Nov 30 '10 at 18:43
  • 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? – Hongxu Chen Sep 24 '14 at 02:02
  • 1
    @HongxuChen In that example there's absolutely no difference, see the quote from the standard in the accepted answer. – Nikola Smiljanić Sep 25 '14 at 03:22
  • Actually, you can use p afterwards, as long as you assign to it. Until then, you can't try to reference the contents. – Alan Mar 15 '17 at 14:06
  • Isn't the copy constructor for `std::unique_ptr` removed to be move-only even RVO still needs it? – DrumM May 06 '19 at 08:00
42

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.

Paul de Vrieze
  • 4,815
  • 1
  • 21
  • 28
Bartosz Milewski
  • 9,181
  • 4
  • 34
  • 45
  • 8
    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". – CB Bailey Nov 30 '10 at 23:12
  • Does wrapping the returned value from the function in std::move mean that it will be moved twice? –  Apr 03 '13 at 21:42
  • 3
    @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 noop – TiMoch Apr 13 '13 at 19:37
  • Thank you. This is what I was looking for. – Vasantha Ganesh Dec 09 '20 at 06:03
15

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.

Praetorian
  • 100,267
  • 15
  • 224
  • 307
David Lee
  • 699
  • 1
  • 9
  • 12
6

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));
v010dya
  • 4,283
  • 5
  • 24
  • 43
  • It is mentioned in the [answer by fredoverflow](/a/4316948) - clearly highlighted "**automatic** object". A reference (including an rvalue reference) is not an automatic object. – Toby Speight Jul 03 '17 at 16:15
  • @TobySpeight Ok, sorry. I guess my code is just a clarification then. – v010dya Jul 03 '17 at 16:33
  • 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. – Nick Alger Apr 24 '21 at 22:16
2

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

Vicky Gupta
  • 396
  • 3
  • 11