使用 auto&& 完美转发 return 值

Perfect-forwarding a return value with auto&&

参考 C++ 模板:完整指南(第 2 版)中的这句话

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.

作者说 auto&& 不适合完美转发 return 值。但是,decltype(auto) 不也是对 xvalue/lvalue 的引用吗? IMO,decltype(auto) 然后遇到同样的问题。那么,作者的意思是什么?

编辑:

上面的代码片段应该放在这个函数模板中。

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

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

没有

decltype(auto) 的部分魔力在于它知道 ret 是一个左值,so it will not form a reference

如果您编写 return (ret),它确实会解析为引用类型,并且您将返回对局部变量的引用。

tl;dr:decltype(auto) 并不总是与 auto&& 相同。

这里有两个推导。一个来自 return 表达式,一个来自 std::invoke 表达式。因为decltype(auto) is deduced to be the declared type for unparenthesized id-expression,我们可以着重从std::invoke表达式推导

引自[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.

并引用自[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.

注意如果 e 是纯右值,则 decltype(e) 被推导为 T 而不是 T&&。这是与auto&&.

的区别

所以如果std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...)是纯右值,比如Callable的return类型不是引用,即returning by value,ret被推导为同一类型而不是引用,完美转发了returning的语义。

auto&& 始终是引用类型。另一方面,decltype(auto) 可以是引用类型或值类型,具体取决于使用的初始化程序。

由于return语句中的ret没有被括号括起来,call()推导出的return类型只依赖于声明的类型实体 ret 以及 值类别 表达式 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;
}

如果Callablereturns by value,那么op的调用表达式的值类别将是一个prvalue。在那种情况下:

  • decltype(auto) 会将 res 推断为 non-reference 类型(即值类型)。
  • auto&& 会推导出 res 作为引用类型。

如上所述,call() 的 return 类型的 decltype(auto)res 的类型相同。因此,如果 auto&& 将被用于推断 res 的类型而不是 decltype(auto),那么 call() 的 return 类型将是对本地对象 ret,在 call() returns.

之后不存在

我有一个类似的问题,但具体是如何正确地 return ret 就像我们直接调用 invoke 而不是 call.

在您显示的示例中,call(A, B) 没有 每个 A 具有相同的 return 类型的 std::invoke(A, B)B.

具体来说,当 invoke return 是 T&&call return 是 T&

你可以在这个例子中看到它(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
}

因此看来正确转发函数的 return 值的唯一方法是

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;
    }
}

这是一个很大的陷阱(我是掉进去才发现的!)。

return T&& 的功能非常罕见,很容易在一段时间内未被发现。