简体   繁体   中英

Perfect-forwarding a return value with auto&&

Consider this quote from C++ Templates: The Complete Guide (2nd Edition) :

 decltype(auto) ret{std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...)}; ... return ret;

Note that declaring ret with auto&& is not correct. As a reference, auto&& extends the lifetime of the returned value until the end of its scope but not beyond the return statement to the caller of the function .

The author says that auto&& is not appropriate for perfect-forwarding a return value. However, doesn't decltype(auto) also form a reference to xvalue/lvalue? IMO, decltype(auto) then suffers from the same issue. Then, what's the point of the author?

EDIT:

The above code snippet shall go inside this function template.

template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
    // here
}

There are two deductions here. One from the return expression, and one from the std::invoke expression. Because decltype(auto) is deduced to be the declared type for unparenthesized id-expression , we can focus on the deduction from the std::invoke expression.

Quoted from [dcl.type.auto.deduct] paragraph 5:

If the placeholder is the decltype(auto) type-specifier , T shall be the placeholder alone. The type deduced for T is determined as described in [dcl.type.simple], as though e had been the operand of the decltype .

And quoted from [dcl.type.simple] paragraph 4 :

For an expression e , the type denoted by decltype(e) is defined as follows:

  • if e is an unparenthesized id-expression naming a structured binding ([dcl.struct.bind]), decltype(e) is the referenced type as given in the specification of the structured binding declaration;

  • otherwise, if e is an unparenthesized id-expression or an unparenthesized class member access, decltype(e) is the type of the entity named by e . If there is no such entity, or if e names a set of overloaded functions, the program is ill-formed;

  • otherwise, if e is an xvalue, decltype(e) is T&& , where T is the type of e ;

  • otherwise, if e is an lvalue, decltype(e) is T& , where T is the type of e ;

  • otherwise, decltype(e) is the type of e .

Note decltype(e) is deduced to be T instead of T&& if e is a prvalue. This is the difference from auto&& .

So if std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...) is a prvalue, for example, the return type of Callable is not a reference, ie returning by value, ret is deduced to be the same type instead of a reference, which perfectly forwards the semantic of returning by value.

However, doesn't decltype(auto) also form a reference to xvalue/lvalue?

No.

Part of decltype(auto) 's magic is that it knows ret is an lvalue, so it will not form a reference .

If you'd written return (ret) , it would indeed have resolved to a reference type and you'd be returning a reference to a local variable.

tl;dr: decltype(auto) is not always the same as auto&& .

auto&& is always a reference type. On the other hand, decltype(auto) can be either a reference or a value type, depending on the initialiser used.

Since ret in the return statement is not surrounded by parenthesis, call() 's deduced return type only depends on the declared type of the entity ret , and not on the value category of the expression ret :

template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
   decltype(auto) ret{std::invoke(std::forward<Callable>(op),
                                  std::forward<Args>(args)...)};
   ...
   return ret;
}

If Callable returns by value , then the value category of op 's call expression will be a prvalue . In that case:

  • decltype(auto) will deduce res as a non-reference type (ie, value type).
  • auto&& would deduce res as a reference type.

As explained above, the decltype(auto) at call() 's return type simply results in the same type as res . Therefore, if auto&& would have been used for deducing the type of res instead of decltype(auto) , call() 's return type would have been a reference to the local object ret , which does not exist after call() returns.

I had a similar question, but specific to how to properly return ret as if we called invoke directly instead of call .

In the example you show, call(A, B) does not have the same return type of std::invoke(A, B) for every A and B .

Specifically, when invoke returns an T&& , call returns a T& .

You can see it in this example ( wandbox link )

#include <type_traits>
#include <iostream>

struct PlainReturn {
    template<class F, class Arg>
    decltype(auto) operator()(F&& f, Arg&& arg) {
        decltype(auto) ret = std::forward<F>(f)(std::forward<Arg>(arg));
        return ret;
    }
};

struct ForwardReturn {
    template<class F, class Arg>
    decltype(auto) operator()(F&& f, Arg&& arg) {
        decltype(auto) ret = std::forward<F>(f)(std::forward<Arg>(arg));
        return std::forward<decltype(ret)>(ret);
    }
};

struct IfConstexprReturn {
    template<class F, class Arg>
    decltype(auto) operator()(F&& f, Arg&& arg) {
        decltype(auto) ret = std::forward<F>(f)(std::forward<Arg>(arg));
        if constexpr(std::is_rvalue_reference_v<decltype(ret)>) {
            return std::move(ret);
        } else {
            return ret;
        }
    }
};

template<class Impl>
void test_impl(Impl impl) {
    static_assert(std::is_same_v<int, decltype(impl([](int) -> int {return 1;}, 1))>, "Should return int if F returns int");
    int i = 1;
    static_assert(std::is_same_v<int&, decltype(impl([](int& i) -> int& {return i;}, i))>, "Should return int& if F returns int&");
    static_assert(std::is_same_v<int&&, decltype(impl([](int&& i) -> int&& { return std::move(i);}, 1))>, "Should return int&& if F returns int&&");
}

int main() {
    test_impl(PlainReturn{}); // Third assert fails: returns int& instead
    test_impl(ForwardReturn{}); // First assert fails: returns int& instead
    test_impl(IfConstexprReturn{}); // Ok
}

So it appears that the only way to properly forward the return value of a function is by doing

decltype(auto) operator()(F&& f, Arg&& arg) {
    decltype(auto) ret = std::forward<F>(f)(std::forward<Arg>(arg));
    if constexpr(std::is_rvalue_reference_v<decltype(ret)>) {
        return std::move(ret);
    } else {
        return ret;
    }
}

This is quite a pitfall (which I discovered by falling into it.).

Functions which return T&& are rare enough that this can easily go undetected for a while.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM