返回转发引用参数 - 最佳实践

14

在下面的情境中

template <class T>
? f(T&& a, T&& b)
{
    return a > b ? a : b; 
}

最佳的返回类型应该是什么? 我目前的想法是:

  1. 使用r值引用进行返回,并完美转发函数参数:

template <class T>
decltype(auto) f(T&& a, T&& b)
{
    return a > b ? forward<T>(a) : forward<T>(b); 
}
  • 移动构造返回值:

    template <class T>
    auto f(T&& a, T&& b)
    {
        return a > b ? forward<T>(a) : forward<T>(b); 
    }
    
  • 尝试找到一种方法来启用(N)RVO(即使我认为,由于我想要使用函数参数,这是不可能的)

  • 这些解决方案有问题吗?有更好的解决方法吗?最佳实践是什么?


    2
    你怎么能期望有一个返回类型可以返回两种不同的类型? T 或 U? - xaxxon
    1
    @xaxxon 假设 T 是 int,U 是 double。那么三元运算符的类型将是什么?我会让你自己思考,你应该能理解我的意思。 - Lorah Attkins
    1
    只要“T”可以转换为“U”(或其反向转换,或根据标准定义的一些更复杂的内容),这就是完全有效的。 - Holt
    1
    @Ajay 因为像 char a,b; decltype(a+b) c; std::cout << sizeof(c); 这样的东西会打印出 4。 - PeterT
    4
    template <class T, class U> decltype(auto) f(T&& a, U&& b) 这样写没问题;或者你可以让它支持 sfinae,template <class T, class U> auto f(T&& a, U&& b) -> decltype(a > b ? forward<T>(a) : forward<U>(b))。在这两种情况下,编译器会根据条件运算符的操作数确定一个通用类型,来计算适当的返回类型(如果有的话)。我认为你不可能获得比这更多的东西了。 - Piotr Skotnicki
    显示剩余15条评论
    2个回答

    7
    对于选项1,您需要使用两个模板参数来启用转发,当两个参数具有不同的值类别时才会发生。应该是:
    template <typename T, typename U>
    decltype(auto) f(T&& a, U&& b)
    {
        return a > b ? std::forward<T>(a) : std::forward<U>(b);
    }
    

    这里实际返回的内容很难确定。首先,您需要知道ab的值类别,以及TU的推断结果。然后,您选择的任何配对都将通过三元运算符的非常复杂的规则。最后,该输出通过decltype规则进行处理,以给出函数的实际返回类型。
    我确实曾经坐下来一次并计算了所有内容,从SO得到了一点帮助。据我最好的回忆,情况是这样的:仅当ab是兼容类型的左值引用时,结果将是可变的左值引用;如果ab中有一个是const左值引用,另一个是任何类型的引用,则结果将是const左值引用;否则,f将返回一个新值。
    换句话说,在任何给定的参数对中,它都会做正确的事情--可能是因为有人坐下来编写了规则,使其在所有情况下都能做正确的事情。
    选项2将完全与选项1相同,只是decltype规则不起作用,函数将始终返回一个新值--普通auto永远不会推导为引用。
    我想不出第三个选项可以通过RVO工作,因为正如您所说,您正在使用参数。
    总而言之,我认为(1)是您要寻找的答案。

    std::forward<T>(b) 是一个打字错误吗? - Piotr Skotnicki

    5

    这取决于您的意图以及您的 T 的移动意识。每种情况都是有效的,但行为上存在差异;这样做:

    template <class T>
    decltype(auto) f(T&& a, T&& b)
    {
        return a > b ? forward<T>(a) : forward<T>(b); 
    }
    

    完美地转发左值或右值引用,不会产生任何临时变量。如果在那里放置了inline(或者编译器为您放置),就像将参数放在原地一样。与“第一眼直觉”相反,它不会因为“引用”死堆栈对象(对于rvalue的情况)而产生任何运行时错误,因为我们转发的任何临时对象都来自我们上面的调用程序(或下面的调用程序,这取决于您如何考虑函数调用期间的堆栈)。另一方面,在rvalue的情况下:

    template <class T>
    auto f(T&& a, T&& b)
    {
        return a > b ? forward<T>(a) : forward<T>(b); 
    }
    

    当使用f时,将会进行值的移动构造(auto 不会推导出 &&&),如果T不可移动构造,则将调用复制构造函数(如果T是左值引用,则始终会进行复制)。在表达式中使用f后,最终会产生一个 xvalue(这让我想知道编译器是否可以使用初始的 rvalue,但我不敢打赌)。

    长话短说,如果使用f的代码以一种处理rvalue和lvalue引用的方式构建,那么我会选择第一种选择,毕竟这就是std::forward所基于的逻辑,此外,当你有左值引用时,不会产生任何副本。


    1
    不,我的意思是当参数表达式的值类别(和类型)不同时,在任何情况下使用两次的 T 都可能导致扣除失败。 - Piotr Skotnicki
    @PiotrSkotnicki 如果存在 TU,我们如何返回对它们中任意一个的引用?(假设它们是不同类型的左值)。强制使用相同的 T(加上推断相同的值类别或在编译时失败)使得返回类型仅基于值类别进行类型切换。 - Nikos Athanasiou
    1
    @PiotrSkotnicki Nikos的假设是正确的。我并不在乎有2、3或4种类型,我只是想知道如何从函数中获取值,是应该将其转发还是移动构造返回值?也许我应该编辑成只有一个参数?(除此之外,在编辑后,f的签名就像给出的那样,我不需要改变它的答案,我假设这个答案是“如果你有那个签名,那么...”) - Lorah Attkins
    1
    @LorahAttkins,这不仅涉及到相等类型的问题,还涉及到相等值类别的问题。如果您想避免混淆,那么使用单参数函数将是一个更好的例子。 - Piotr Skotnicki
    1
    我同意@PiotrSkotnicki的观点。我认为这个答案并不是很有用,因为像这样的简单代码无法编译。 - ildjarn
    显示剩余7条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接