通过输入参数多次传递完美转发

时间:2013-11-25 14:44:57

标签: c++ c++11 move-semantics rvalue-reference perfect-forwarding

考虑以下函数accept,该函数采用类型为T的“通用引用”,并将其转发给parse<T>()函数对象,其中lvalues为重载,rvalues为1:

template<class T>
void accept(T&& arg)
{
    parse<T>()(std::forward<T>(arg), 0); // copy or move, depending on rvaluedness of arg
}

template<class T>
class parse
{
    // parse will modify a local copy or move of its input parameter
    void operator()(T const& arg, int n) const { /* optimized for lvalues */ }
    void operator()(T&& arg)    , int n) const { /* optimized for rvalues */ }
};

由于完美转发使源对象处于有效但未定义的状态,因此无法在同一范围内再次完美转发。下面我尝试在一个假设的split()函数中尽可能少地复制,该函数使用int代表必须对输入数据进行的数字传递:

template<class T>
void split(T&& arg, int n)
{
    for (auto i = 0; i < n - 1; ++i)
        parse<T>()(arg , i);                 // copy n-1 times
    parse<T>()(std::forward<T>(arg), n - 1); // possibly move the n-th time
}

问题:这是在同一数据上多次传递应用完美转发的推荐方法吗?如果没有,那么最小化副本数量的更惯用的方法是什么?

2 个答案:

答案 0 :(得分:8)

  

问题:这是在同一数据上多次传递应用完美转发的推荐方法吗?

是的,当您需要多次传递数据时,这是应用完美转发(或移动)的推荐方法。只有(可能)在您上次访问时从中移动。实际上,这种情况是在original move paper中预见的,并且非常的原因是,使用类型rvalue-reference声明的“命名”变量不会被隐式移动。来自N1377

  

即使命名的右值引用可以绑定到右值,它们也是   使用时作为左值处理。例如:

struct A {};

void h(const A&);
void h(A&&);

void g(const A&);
void g(A&&);

void f(A&& a)
{
    g(a);  // calls g(const A&)
    h(a);  // calls h(const A&)
}
  

虽然rvalue可以绑定到f()的“a”参数,但一旦绑定,a   现在被视为左值。特别是,调用重载   函数g()和h()解析为const A&amp; (左值)超载。   将“a”视为f中的右值将导致容易出错的代码:   首先,将调用g()的“移动版本”,这很可能   窃取“a”,然后被盗的“a”将被发送到移动   h()的重载。

如果您想在上面的示例中移动h(a),则必须明确地执行此操作:

    h(std::move(a));  // calls h(A&&);

正如Casey在评论中指出的那样,传入左值时会出现重载问题:

#include  <utility>
#include  <type_traits>

template<class T>
class parse
{
    static_assert(!std::is_lvalue_reference<T>::value,
                               "parse: T can not be an lvalue-reference type");
public:
    // parse will modify a local copy or move of its input parameter
    void operator()(T const& arg, int n) const { /* optimized for lvalues */ }
    void operator()(T&& arg     , int n) const { /* optimized for rvalues */ }
};

template<class T>
void split(T&& arg, int n)
{
    typedef typename std::decay<T>::type Td;
    for (auto i = 0; i < n - 1; ++i)
        parse<Td>()(arg , i);                 // copy n-1 times
    parse<Td>()(std::forward<T>(arg), n - 1); // possibly move the n-th time
}

上面我已经按照Casey的建议修复了它,只使用parse<T>在非引用类型上实例化std::decay。我还添加了一个static_assert,以确保客户端不会意外地犯这个错误。 static_assert并非绝对必要,因为无论如何都会遇到编译时错误。但是static_assert可以提供更易读的错误消息。

这不是解决问题的唯一方法。另一种允许客户端使用左值引用类型实例化parse的方法是部分特化解析:

template<class T>
class parse<T&>
{
public:
    // parse will modify a local copy or move of its input parameter
    void operator()(T const& arg, int n) const { /* optimized for lvalues */ }
};

现在客户端不需要进行decay舞蹈:

template<class T>
void split(T&& arg, int n)
{
    for (auto i = 0; i < n - 1; ++i)
        parse<T>()(arg , i);                 // copy n-1 times
    parse<T>()(std::forward<T>(arg), n - 1); // possibly move the n-th time
}

如果需要,您可以在parse<T&>下应用特殊逻辑。

答案 1 :(得分:0)

(我知道,这是一个旧线程)

如评论中所述,数据是uint64_t的大数组或向量。比防止最终复制的参数传递更好的优化方法可能是优化许多复制操作以

  • 阅读一次
  • 多次书写(针对每个预期通过)

一步一步完成,而不是许多独立副本。

faster alternative to memcpy?的起点可能是包含类似memcpy的代码的答案。您必须将写入目标的代码行乘以写入数据的多个副本。

您还可以结合使用memset和memcpy,memset可以优化一次又一次地将相同的值写入内存,而memcpy则可以优化每个存储块一次读写存储器的值。您可以在此处查看优化的源代码:https://github.com/KNNSpeed/AVX-Memmove

最好的代码将特定于所使用的体系结构和处理器。因此,您必须测试并比较您所达到的速度。