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

问题描述 投票:5回答:2

考虑以下函数 accept 类型的 "通用引用"。T 并将其转发给 parse<T>() 函数对象,其中lvalues和rvalues分别有一个重载。

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
}

问题问:这是否是对同一数据进行多次传递的完美转发的推荐方式?如果不是,有什么更习惯的方法来减少副本的数量?

c++ c++11 move-semantics rvalue-reference perfect-forwarding
2个回答
9
投票

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

是的,当你需要多次传递数据时,这是应用完美转发(或移动)的推荐方式。 只有在你最后一次访问时才(可能)从中移动。 事实上,这种情况在《》中已经预见到了。原招,并且是 的原因,用rvalue-reference类型声明的 "命名 "变量不会被隐式地从。 从 N1377:

尽管命名的rvalue引用可以绑定到rvalue上,但在使用时,会被视为lvalue。比如说:虽然一个r值可以绑定到f()的 "a "参数上,但是一旦绑定,a就会被视为l值。

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就会被视为lvalue。特别是,对重载函数g()和h()的调用解析为const A&(lvalue)重载。 在f中把 "a "作为r值处理,会导致容易出错的代码。 首先,g()的 "移动版本 "会被调用,这很可能会窃取 "a",然后被窃取的 "a "会被发送到h()的移动重载。

如果你想让 "移动版 "的g()被调用,那么很可能会盗取 "a",然后盗取的 "a "会被发送到h()的移动重载中。h(a) 在上面的例子中,你必须明确地这样做。

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

As 凯西 在评论中指出,你在传递l值时有一个重载问题。

#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 与lvalue引用类型,是为了部分特殊化解析。

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&> 如果需要的话。


0
投票

(我知道,这是个老话题)

正如评论中所说,数据是一个大数组或uint64_t的向量。比参数传递更好的优化是防止最后的复制,可能是将许多复制操作优化为

  • 一读
  • 多写

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

一个起点可以是这样的 更快地替代memcpy? 的答案,其中包括类似memcpy的代码。你将不得不乘以写到目标的代码行来代替写几份数据。

你也可以将memset和memcpy结合在一起,memset是为重复向内存中写入相同的值而优化的,而memcpy则是为每个内存块读写一次而优化的。你可以在这里看看优化后的源码。https:/github.comKNNSpeedAVX-Memmove。

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

© www.soinside.com 2019 - 2024. All rights reserved.