5

考虑以下函数,该函数accept采用类型的“通用引用”T并将其转发到具有parse<T>()左值重载和右值重载的函数对象:

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
}

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

4

2 回答 2

9

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

是的,当您需要多次传递数据时,这是应用完美转发(或移动)的推荐方式。只有(可能)在您最后一次访问时离开它。事实上,这种情况在最初的 move 论文中已经预见到了,这正是使用 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&)
}

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

如果你想h(a)在上面的例子中移动,你必须明确地这样做:

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

正如凯西在评论中指出的那样,传入左值时会遇到重载问题:

#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&>如有必要,您可以应用特殊逻辑。

于 2013-11-25T17:52:43.240 回答
0

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

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

  • 读一次
  • 写很多次(对于每个预期的通行证)

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

一个起点可能是这个更快的 memcpy 替代方案?其中的答案包括类似 memcpy 的代码。您必须将写入目标的代码行乘以写入多个数据副本。

您还可以将 memset 和 memcpy 结合使用,memset 为反复将相同的值写入内存而优化,memcpy 为每个块读取和写入内存块一次而优化。您可以在这里查看优化的源代码:https ://github.com/KNNSpeed/AVX-Memmove

最好的代码将特定于所使用的架构和处理器。所以你必须测试和比较你达到的速度。

于 2020-06-03T06:11:35.107 回答