524

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?

Jan Schultke
43.6k8 gold badges108 silver badges188 bronze badges
asked Nov 30, 2010 at 17:44
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. Commented Sep 15, 2015 at 11:10
  • 8
    @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. Commented Sep 15, 2015 at 16:28
  • 1
    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. Commented Jan 19, 2019 at 14:17

7 Answers 7

296

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
21.3k11 gold badges80 silver badges168 bronze badges
answered Nov 30, 2010 at 18:06
Sign up to request clarification or add additional context in comments.

4 Comments

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_optimization
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?
I found the issue finally, it was caused by a member not being moveable ;-)
Where can we find worst case ... is treated as an rvalue. in the std reference? Thx!
145

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
15.1k4 gold badges36 silver badges53 bronze badges
answered Nov 30, 2010 at 17:48

6 Comments

@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.
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.
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?
@HongxuChen In that example there's absolutely no difference, see the quote from the standard in the accepted answer.
Actually, you can use p afterwards, as long as you assign to it. Until then, you can't try to reference the contents.
|
48

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,9181 gold badge27 silver badges29 bronze badges
answered Nov 30, 2010 at 23:00

3 Comments

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".
Does wrapping the returned value from the function in std::move mean that it will be moved twice?
@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
29

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
110k20 gold badges248 silver badges340 bronze badges
answered Oct 4, 2017 at 18:38

2 Comments

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;
@Will: The first is RVO (which in C++17 is no longer an optimization enabled by a language rule, but a required feature of the language); the second is NRVO (which is still allowed but optional).
12

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

answered Jun 22, 2020 at 12:42

3 Comments

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.
It does not seem like the document you referenced says std::move() should be used in this case. I think it is the opposite.
The rule for this case has changed several times. C++20 allows the types to be different; C++23 simply says the id-expression is an xvalue in a return.
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));
answered Jul 3, 2017 at 16:06

6 Comments

It is mentioned in the answer by fredoverflow - clearly highlighted "automatic object". A reference (including an rvalue reference) is not an automatic object.
@TobySpeight Ok, sorry. I guess my code is just a clarification then.
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.
May I ask why std::move() is necessary in foo2 ?
In C++20, this std::move too may be omitted (a local rvalue reference is treated as an xvalue when it is the operand of a return statement).
|
5

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.

answered Oct 4, 2022 at 10:48

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.