【问题标题】:perfect forwarding and order of function arguments evaluation函数参数评估的完美转发和顺序
【发布时间】:2013-07-15 04:09:46
【问题描述】:

在这样的情况下,有没有一种好方法可以将函数 f 的参数转发给函数 g

template<typename... T>
void g(T && ... x);

template<typename... T>
void f(T && ... x)
{
    g(x..., x...);
}

在下一个代码中x 可以移动两次

template<typename... T>
void f(T && ... x)
{
    g(std::forward<T>(x)..., std::forward<T>(x)...);
}

在下一个代码中,std::forward&lt;T&gt;(x)... 可以在 x... 之前进行评估

template<typename... T>
void f(T && ... x)
{
    g(x..., std::forward<T>(x)...);
}

【问题讨论】:

  • 您不能将多个相同的引用传递给另一个函数,至少有一个是右值引用,并且不要期望会发生可怕的事情。通过const&amp;取参数。
  • 如果“g”被内联而不是“yes”。如果 G 是一个模板,那么如果编译器认为这是一个好主意,它将被内联。而且编译器比你更聪明(当涉及到内联时),所以通常最好让它自己做。
  • 或者 std::move,如果你想这样做也可以,但是天哪,这是个坏主意。
  • g(x..., x...) 到底有什么问题?你真的不能做得比这更好,因为无论如何移动都是一个问题。

标签: c++ templates c++11 rvalue-reference


【解决方案1】:

std::forward 不会移动任何东西 - 它会创建一个引用,上面写着“可以从我身边移动”。实际移动发生在 g 内部,而不是在调用 std::forwardstd::movef 中。

move 的问题只是这里的问题之一。还有在两个点将同一个对象作为引用传递两次的问题,这通常被认为是相当粗鲁的!

我们可以通过在f 中创建临时对象并通过引用传递它们来解决这个问题,但这会导致一个严重的问题:引用通常用于从函数返回值,并且我们有两次使用相同的变量 - - 我们不能同时返回两个结果。

所以答案是“不要那样做”,因为它通常不安全。你必须知道gf 的语义才能弄清楚正确的做法是什么,简单的转发类型的接口不会反映所需的知识深度。

如果你对 gf 应该做什么有深刻的语义理解,那么情况就会改变。

【讨论】:

  • “引用通常用于从函数返回值” - 等等,什么?
  • bool try_fill_vector( std::vector&lt;int&gt;&amp; fill_this ) -- 引用值fill_this 用于从函数返回值。它不是“返回值”,但很多时候都是通过引用参数返回值。现在假设我们有void split_vector( std::vector&lt;int&gt; const&amp; src, int split, std::vector&lt;int&gt;&amp; lower, std::vector&lt;int&gt;&amp; upper)——如果我们将相同的左值传递给lowerupper,那么函数的行为就会变得非常糟糕。
  • 我被return 的使用严重抛弃了:) 我明白你的意思了,这个例子没那么必要了:/
【解决方案2】:

您通常可以使用

强制订单
  • 使用单独的语句(显然)
  • 用逗号分隔的表达式。 (注意过载operator,
  • 使用大括号初始化是有效的,因为大括号初始化列表中参数的求值顺序是它们出现的顺序1。以下具有明确定义的评估顺序:

    std::tuple<T..., T...> args { 
         std::forward<T>(x)..., 
         std::forward<T>(x)... }; // still not sane, but evaluation order defined
    

但它仍然没用,因为g(...) 可能仍然会从同一个引用移动两次。您真正想要的右值引用是不是

g(rvalue, std::move(rvalue));            // or
g(std::move(rvalue), rvalue);            // or even
g(std::move(rvalue), std::move(rvalue)); // [sic]

唯一理智的方法是:

g(lvalue=std::move(rvalue), lvalue); // BUT: fix the evaluation order

那么我们如何精确地 实现但一般地?

输入Indices?!

假设您有可变参数 g,正如您所描述的:

template<typename... T>
void g(T && ... x) 
{ 
}

现在,您可以使用

复制传递给 f 的参数
  1. 索引技巧:

    namespace detail // indices 
    {
        template<std::size_t... Is> struct seq{};
        template<std::size_t I, std::size_t... Is>
            struct gen_seq : gen_seq<I-1, I-1, Is...>{};
        template<std::size_t... Is>
            struct gen_seq<0, Is...>{ using type = seq<Is...>; };
    }
    
  2. 和一个调用者辅助函数

    #include <tuple>
    
    template<typename Tuple, std::size_t... Is>
    void f_invoke_helper(Tuple const& tup, detail::seq<Is...>) 
    { 
        g(std::get<Is>(tup)..., std::get<Is>(tup)...);
    }
    

接下来要做的就是将它们捆绑在一起:

template<typename... T>
void f(T && ... x)
{
    f_invoke_helper(
            std::make_tuple(std::forward<T>(x)...), 
            typename detail::gen_seq<sizeof...(T)>::type());
}

请注意,如果您传递 rvalue-refs,它将被 移动 一次(进入元组)并在调用程序助手中使用两次(作为左值):

int main()
{
    std::string x = "Hello world";
    int i = 42;

    // no problem:
    f(i, -42, std::move(x));
}

希望这会有所帮助!

PS。正如已经恰当地指出的那样,说起来可能要容易得多

template<typename... T>
void f(T&&... x) { g(x..., x...); }

除了实际上将可移动参数移动到元组中之外,我还没有想到元组习语不会产生相同结果的方法。


112.6.1 中描述了 T{...} 的语义
另请参阅:how to avoid undefined execution order for the constructors when using std::make_tuple

【讨论】:

  • 如果g 从某物的第一个引用移开,那么第二个引用就会被弄乱。根本问题是 g 只是为每个参数接收一个别名参数,并且不可能在不影响另一个参数的情况下安全地对一个参数进行操作。
  • @GManNickG 我认为你错过了元组的重点?关于别名:函数foo(int&amp;, int&amp;) 也是如此。 C++ 假设程序员知道他在做什么。
  • 请问您可以复制传递给f的参数是什么意思?
  • @MaximYegorushkin 这是 OP 要求的。阅读原帖第 7 行代码。
  • @sehe 你是对的,OP 想要复制或克隆 arg。
猜你喜欢
  • 1970-01-01
  • 2018-11-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多