简体   繁体   English

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

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

Consider this quote from C++ Templates: The Complete Guide (2nd Edition) :考虑一下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.请注意,用auto&&声明ret是不正确的。 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语句

The author says that auto&& is not appropriate for perfect-forwarding a return value.作者说auto&&不适合完美转发返回值。 However, doesn't decltype(auto) also form a reference to xvalue/lvalue?但是, decltype(auto)不也形成了对 xvalue/lvalue 的引用吗? IMO, decltype(auto) then suffers from the same issue. IMO, decltype(auto)然后遇到同样的问题。 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.一个来自 return 表达式,一个来自std::invoke表达式。 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.因为decltype(auto)被推导为未加括号的 id-expression 的声明类型,我们可以专注于从std::invoke表达式推导。

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

If the placeholder is the decltype(auto) type-specifier , T shall be the placeholder alone.如果占位符是decltype(auto)类型说明符,则T应单独作为占位符。 The type deduced for T is determined as described in [dcl.type.simple], as though e had been the operand of the decltype .T推导的类型如 [dcl.type.simple] 中所述确定,就好像edecltype的操作数一样。

And quoted from [dcl.type.simple] paragraph 4 :并引用自[dcl.type.simple] 第 4 段

For an expression e , the type denoted by decltype(e) is defined as follows:对于表达式edecltype(e)表示的类型定义如下:

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

  • 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 .否则,如果e是未加括号的id 表达式或未加括号的类成员访问,则decltype(e)e命名的实体的类型。 If there is no such entity, or if e names a set of overloaded functions, the program is ill-formed;如果没有这样的实体,或者如果e命名了一组重载函数,则程序是病式的;

  • otherwise, if e is an xvalue, decltype(e) is T&& , where T is the type of e ;否则,如果e是一个 xvalue,则decltype(e)T&& ,其中Te的类型;

  • otherwise, if e is an lvalue, decltype(e) is T& , where T is the type of e ;否则,如果e是左值,则decltype(e)T& ,其中Te的类型;

  • otherwise, decltype(e) is the type of e .否则, decltype(e)e的类型。

Note decltype(e) is deduced to be T instead of T&& if e is a prvalue.注意decltype(e)被推导为T而不是T&&如果e是纯右值。 This is the difference from auto&& .这是与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.所以如果std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...)是纯右值,例如Callable的返回类型不是引用,即按值返回, ret被推导为同类型而不是引用,完美转发了按值返回的语义。

However, doesn't decltype(auto) also form a reference to xvalue/lvalue?但是,decltype(auto) 不也形成了对 xvalue/lvalue 的引用吗?

No.不。

Part of decltype(auto) 's magic is that it knows ret is an lvalue, so it will not form a reference . decltype(auto)的部分魔力在于它知道ret是左值,因此它不会形成引用

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.如果您编写了return (ret) ,它确实会解析为引用类型,并且您将返回对局部变量的引用。

tl;dr: decltype(auto) is not always the same as auto&& . tl;dr: decltype(auto)并不总是与auto&&相同。

auto&& is always a reference type. auto&&始终是引用类型。 On the other hand, decltype(auto) can be either a reference or a value type, depending on the initialiser used.另一方面, decltype(auto)可以是引用类型或值类型,具体取决于所使用的初始化程序。

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 :由于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;
}

If Callable returns by value , then the value category of op 's call expression will be a prvalue .如果Callable按值返回,则op的调用表达式的值类别将为纯右值 In that case:在这种情况下:

  • decltype(auto) will deduce res as a non-reference type (ie, value type). decltype(auto)会将res推断为非引用类型(即值类型)。
  • auto&& would deduce res as a reference type. auto&&会将res推断为引用类型。

As explained above, the decltype(auto) at call() 's return type simply results in the same type as res .如上所述, call()的返回类型中的decltype(auto)只会导致与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.因此,如果使用auto&&而不是decltype(auto)来推断res的类型,则call()的返回类型将是对本地对象ret的引用,该对象在call()返回后不存在。

I had a similar question, but specific to how to properly return ret as if we called invoke directly instead of call .我有一个类似的问题,但具体到如何正确返回ret就像我们直接invoke而不是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 .在您显示的示例中, call(A, B) , B) 对于每个AB没有std::invoke(A, B)相同的返回类型。

Specifically, when invoke returns an T&& , call returns a T& .具体来说,当invoke返回T&&时, call返回T&

You can see it in this example ( wandbox link )你可以在这个例子中看到它( 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
}

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.返回T&&的函数很少见,很容易在一段时间内不被发现。

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

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