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?

o_oTurtle
  • 1,091
  • 3
  • 12

1 Answers1

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.

alagner
  • 3,448
  • 1
  • 13
  • 25
  • Oh, I understand. It's really counter-intuitive for me that `static_cast(arg)` will disable copy elision even though it in fact does nothing. Thank you! – o_oTurtle Feb 08 '23 at 15:43