Guillaume Racicot Guillaume Racicot - 1 month ago 15
C++ Question

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.

Answer

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.