3

Sometimes we may defer perfect returning like this:

template<typename Func, typename... Args> decltype(auto) call(Func f, Args&&... args) { decltype(auto) ret{f(std::forward<Args>(args)...)}; // ... return static_cast<decltype(ret)>(ret); } 

But in Jousttis's new book C++ Move Semantics - The Complete Guide, he says that code below is better:

template<typename Func, typename... Args> decltype(auto) call(Func f, Args&&... args) { decltype(auto) ret{f(std::forward<Args>(args)...)}; // ... if constexpr (std::is_rvalue_reference_v<decltype(ret)>) { return std::move(ret); // move xvalue returned by f() to the caller } else { return ret; // return the plain value or the lvalue reference } } 

Because the first piece of code "might disable move semantics and copy elision. For plain values, it is like having an unnecessary std::move() in the return statement." What's the difference between these two patterns? From my point of view, for plain values, decltype will deduce just the type itself, so it's just a static_cast<Type>(ret)(i.e. no operation at all) and the returned type is same as the declared type so that copy elision is possible. Is there anything that I take wrong?

1 Answer 1

4

I don't know what edition of the book you have, but mine states explicitly:

perfect return but unnecessary copy

The problem does not manifest for references, however it does when returning by value comes into play. Consider the following code:

#include <iostream> struct S { S() { std::cout << "constr\n";} S(const S& ) { std::cout << "copy constr\n"; } S(S&& ) { std::cout << "move constr\n"; } }; S createSNoElide() { S s; return static_cast<decltype(s)>(s); } S createSElide() { S s; return s; } int main(int, char*[]) { std::cout << "Elision\n"; S s1 = createSElide(); std::cout << "No elision\n"; S s2 = createSNoElide(); } 

https://godbolt.org/z/YqG54rM9E

The createSNoElide() will be forced to use a copy constructor. Standard-wise it is most likely due to the following part:

This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

in a return statement in a function with a class return type, when the expression is the name of a non-volatile object with automatic storage duration (other than a function parameter or a variable introduced by the exception-declaration of a handler ([except.handle])) with the same type (ignoring cv-qualification) as the function return type, the copy/move operation can be omitted by constructing the object directly into the function call's return object

https://eel.is/c++draft/class.copy.elision

I.e. the only way for elision to occur is to return a name of a local variable. Cast is simply a different type of expression, which effectively prevents elision, however counter-intuitive that might be.

Also, I should give extra credit to that post: https://stackoverflow.com/a/55491382/4885321 which guided me to the exact place in the standard.

Sign up to request clarification or add additional context in comments.

1 Comment

Oh, I understand. It's really counter-intuitive for me that static_cast<Type>(arg) will disable copy elision even though it in fact does nothing. Thank you!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.