Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Returning local unique_ptr as a shared_ptr

I'm used to not use std::move when returning a std::unique_ptr, because doing so prohibits RVO. I have this case where I have a local std::unique_ptr, but the return type is a std::shared_ptr. Here's a sample of the code:

shared_ptr<int> getInt1() {
    auto i = make_unique<int>();

    *i = 1;

    return i;
}

shared_ptr<int> getInt2() {
    return make_unique<int>(2);
}

unique_ptr<int> getInt3() {
    auto ptr = make_unique<int>(2);

    return ptr;
}

int main() {
    cout << *getInt1() << endl << *getInt2() << *getInt3() << endl;
    return 0;
}

GCC accepts both cases, but Clang refuses the getInt1() With this error:

main.cpp:10:13: error: no viable conversion from 'std::unique_ptr<int, std::default_delete<int> >' to 'shared_ptr<int>'
    return i;
           ^

Here's both cases on coliru: GCC, Clang

Both compiler accept the third case.

Which one is wrong? Thanks.

like image 745
Guillaume Racicot Avatar asked Mar 10 '16 02:03

Guillaume Racicot


People also ask

What happens when you return a unique_ptr?

If a function returns a std::unique_ptr<> , that means the caller takes ownership of the returned object. class Base { ... }; class Derived : public Base { ... }; // Foo takes ownership of |base|, and the caller takes ownership of the returned // object.

Should I use unique_ptr or shared_ptr?

Use unique_ptr when if you want to have single ownership(Exclusive) of resource. Only one unique_ptr can point to one resource. Since there can be one unique_ptr for single resource its not possible to copy one unique_ptr to another. Use shared_ptr if you want to share ownership of resource .

Can you return a shared_ptr?

So the best way to return a shared_ptr is to simply return by value: shared_ptr<T> Foo() { return shared_ptr<T>(/* acquire something */); }; This is a dead-obvious RVO opportunity for modern C++ compilers. I know for a fact that Visual C++ compilers implement RVO even when all optimizations are turned off.

Can you copy a unique_ptr?

A unique_ptr does not share its pointer. It cannot be copied to another unique_ptr , passed by value to a function, or used in any C++ Standard Library algorithm that requires copies to be made. A unique_ptr can only be moved.


2 Answers

std::unique_ptr could be used to construct std::shared_ptr only when it's a rvalue. See the constructor declaration of std::shared_ptr:

template< class Y, class Deleter >
shared_ptr( std::unique_ptr<Y,Deleter>&& r );

So you need to use std::move to make the 1st case work, otherwise it should fail.

return std::move(i);

BTW: I compiled the code with gcc 4.9.3 it failed either.

source_file.cpp:14:12: error: cannot bind ‘std::unique_ptr<int, std::default_delete<int> >’ 
lvalue to ‘std::unique_ptr<int, std::default_delete<int> >&&’
     return i;
            ^
like image 36
songyuanyao Avatar answered Sep 19 '22 08:09

songyuanyao


The correct answer depends on which C++ standard you are talking about.

If we are talking about C++11, clang is correct (an explicit move is needed). If we are talking about C++14, gcc is correct (an explicit move is not needed).

C++11 says in N3290/[class.copy]/p32:

When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, 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. If overload resolution fails, ...

This demands that you only get the implicit move when the return expression has the same type as the function return type.

But CWG 1579 changed this, and this defect report was accepted after C++11, and in time for C++14. This same paragraph now reads:

When the criteria for elision of a copy/move operation are met, but not for an exception-declaration, and the object to be copied is designated by an lvalue, or when the expression in a return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, ...

This modification basically allows the return expression type to be convertible-to the function return type and still be eligible for implicit move.

Does this mean that the code needs a #if/#else based on the value of __cplusplus?

One could do that, but I wouldn't bother. If I were targeting C++14, I would just:

return i;

If the code is unexpectedly run under a C++11 compiler, you will be notified at compile-time of the error, and it is trivial to fix:

return std::move(i);

If you are just targeting C++11, use the move.

If you want to target both C++11 and C++14 (and beyond), use the move. The downside of using move gratuitously is that you can inhibit RVO (Return Value Optimization). However, in this case, RVO is not even legal (because of the conversion from the return statement to the return type of the function). And so the gratuitous move does not hurt anything.

The one time you might lean towards a gratuitous move even when targeting C++14 is if without it, things still compile in C++11, and invoke an expensive copy conversion, as opposed to a move conversion. In this case, accidentally compiling under C++11 would introduce a silent performance bug. And when compiled under C++14 the gratuitous move still has no detrimental effects.

like image 142
Howard Hinnant Avatar answered Sep 21 '22 08:09

Howard Hinnant