16

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.

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • 2
    I suspect that we'll be able to give better answers when we understand what you need this for. At the moment, this could trivially be solved by calling make_shared instead of make_unique. I assume there's some reason why that's not possible, and it would help to understand that reason. – Elliott Mar 10 '16 at 02:18
  • 7
    @Elliott: What could be solved? He's not asking how to solve anything, he's asking if the code he has is correct by the standard. – Benjamin Lindley Mar 10 '16 at 02:28

2 Answers2

18

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.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • If you are using just C++11 I would encourage to create the correct type before returning rather than having "return std::move( ..." in your code. Because then the compiler might just do NRVO instead of moving a few times. – Dominik Grabiec Sep 19 '16 at 16:05
10

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;
            ^
songyuanyao
  • 169,198
  • 16
  • 310
  • 405