繁体   English   中英

使用 auto&& 完美转发返回值

[英]Perfect-forwarding a return value with auto&&

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

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

请注意,用auto&&声明ret是不正确的。 作为参考, auto&&将返回值的生命周期延长到其范围的末尾,但不会超出函数调用者的return语句

作者说auto&&不适合完美转发返回值。 但是, decltype(auto)不也形成了对 xvalue/lvalue 的引用吗? IMO, decltype(auto)然后遇到同样的问题。 那么,作者的意义何在?

编辑:

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

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

这里有两个扣除。 一个来自 return 表达式,一个来自std::invoke表达式。 因为decltype(auto)被推导为未加括号的 id-expression 的声明类型,我们可以专注于从std::invoke表达式推导。

引自[dcl.type.auto.deduct] 第 5 段:

如果占位符是decltype(auto)类型说明符,则T应单独作为占位符。 T推导的类型如 [dcl.type.simple] 中所述确定,就好像edecltype的操作数一样。

并引用自[dcl.type.simple] 第 4 段

对于表达式edecltype(e)表示的类型定义如下:

  • 如果e是命名结构化绑定 ([dcl.struct.bind]) 的未加括号的 id 表达式,则decltype(e)是结构化绑定声明规范中给出的引用类型;

  • 否则,如果e是未加括号的id 表达式或未加括号的类成员访问,则decltype(e)e命名的实体的类型。 如果没有这样的实体,或者如果e命名了一组重载函数,则程序是病式的;

  • 否则,如果e是一个 xvalue,则decltype(e)T&& ,其中Te的类型;

  • 否则,如果e是左值,则decltype(e)T& ,其中Te的类型;

  • 否则, decltype(e)e的类型。

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

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

但是,decltype(auto) 不也形成了对 xvalue/lvalue 的引用吗?

不。

decltype(auto)的部分魔力在于它知道ret是左值,因此它不会形成引用

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

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

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

由于return语句中的ret没有被括号包围,因此call()的推导返回类型仅取决于实体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;
}

如果Callable按值返回,则op的调用表达式的值类别将为纯右值 在这种情况下:

  • decltype(auto)会将res推断为非引用类型(即值类型)。
  • auto&&会将res推断为引用类型。

如上所述, call()的返回类型中的decltype(auto)只会导致与res相同的类型。 因此,如果使用auto&&而不是decltype(auto)来推断res的类型,则call()的返回类型将是对本地对象ret的引用,该对象在call()返回后不存在。

我有一个类似的问题,但具体到如何正确返回ret就像我们直接invoke而不是call一样。

在您显示的示例中, call(A, B) , B) 对于每个AB没有std::invoke(A, B)相同的返回类型。

具体来说,当invoke返回T&&时, call返回T&

你可以在这个例子中看到它( wandbox 链接

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

所以看起来正确转发函数返回值的唯一方法是做

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

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

返回T&&的函数很少见,很容易在一段时间内不被发现。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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