【问题标题】:Idiomatic way of providing constructors that move their arguments提供移动参数的构造函数的惯用方式
【发布时间】:2018-02-09 14:41:00
【问题描述】:

假设我有以下课程:

#include <vector>
class Foo
{
public:
    Foo(const std::vector<int> & a, const std::vector<int> & b)
        : a{ a }, b{ b } {}
private:
    std::vector<int> a, b;
};

但现在我想说明构造函数的调用者可能将临时对象传递给它的情况,并且我想将这些临时对象正确移动到 ab

现在我真的必须再添加 3 个构造函数吗,其中 1 个以 a 作为右值引用,其中 1 个以 b 作为右值引用,1 个只有右值引用参数?

当然,这个问题可以推广到任何数量的值得移动的参数,并且所需的构造函数的数量将是 arguments^2 2^arguments。

这个问题也适用于所有函数。

这样做的惯用方式是什么?还是我在这里完全遗漏了一些重要的东西?

【问题讨论】:

  • 我见过的一个解决方案是按值获取两个参数,然后移动它们。我看到这个有争议,但我没有来源了:/
  • 哦,是的,你是对的。我将编辑我的答案:p。

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


【解决方案1】:

通常的做法是按值传递,然后从参数中移动构造成员:

Foo(std::vector<int> a, std::vector<int> b)
    : a{ std::move(a) },
      b{ std::move(b) }
{}

如果需要副本,它将由调用者创建,然后移出以构造成员。如果调用者传递一个临时值(或其他右值),则不会复制,只会移动一次。

对于没有有效移动构造函数的参数,接受对 const 的引用会稍微高效一些,我会保留它。

如果函数不需要传递值的副本,则这些都不适用 - 如果您不修改值并且不需要它存在,请继续使用 const ref在函数执行结束之后。就个人而言,我在构造函数中大量使用 pass-by-value-and-move,但很少在其他函数中使用。

【讨论】:

  • 确实临时对象会被移动,但是用左值调用这个ctor会稍微慢一些,因为它被复制和移动而不是被复制?
  • 有点,但想法是移动构造预计会非常便宜。对于std::vector,您将很难衡量差异。而且它比编写 2^n 个不同的构造函数更容易和更容易出错!
  • 这种方法对vector 来说很划算,因为与复制相比,移动是如此便宜。请注意将这种方法推广到您可能没有此特征的其他类型。
  • @Jupiter:没有一种正确的方法可以做到这一点。对于每个单独的案例,我计算副本和移动,并猜测每个案例的成本。有 3 种方法,每种方法都有优点和缺点。这一个,2^n 解决方案(非常适合单参数)和受约束的模板解决方案(完美的性能,但读写难看,容易出错,而且经常过度杀伤)。对于vector,我推荐按值方法。
  • 翻新旧房子通常比从头开始建造新房子更困难、更昂贵。它需要不同的技能,结果也不一样。但有时装修是唯一可行的解​​决办法,特别是如果你不能在工作完成时把人赶出家门。
【解决方案2】:

真的,如果移动构造非常便宜,你应该按价值计算。

这导致在每种情况下都比理想情况多移动 1 次。

但如果你真的必须避免这种情况,你可以这样做:

template<class T>
struct sink_of {
  void const* ptr = 0;
  T(*fn)(void const*) = 0;
  sink_of(T&& t):
    ptr( std::addressof(t) ),
    fn([](void const*ptr)->T{
      return std::move(*(T*)(ptr));
    })
  {}
  sink_of(T const& t):
    ptr( std::addressof(t) ),
    fn([](void const*ptr)->T{
      return *(T*)(ptr);
    })
  {}
  operator T() const&& {
    return fn(ptr);
  }
};

它使用 RVO/elision 来避免以大量基于指针的开销和类型擦除为代价的额外移动。

Here 是一些证明这一点的测试代码

test( noisy nin ):n(std::move(nin)) {}
test( sink_of<noisy> nin ):n(std::move(nin)) {}

相差一个 noisy 的精确移动构造。

“完美”版本

test( noisy const& nin ):n(nin) {}
test( noisy && nin ):n(std::move(nin)) {}

template<class Noisy, std::enable_if_t<std::is_same<noisy, std::decay_t<Noisy>>{}, int> = 0 >
test( Noisy && nin ):n(std::forward<Noisy>(nin)) {}

sink_of 版本具有相同数量的复制/移动。

noisy 是一种打印有关其所从事的移动/复制的信息的类型,因此您可以看到通过省略优化了哪些内容)

只有当额外的move 需要消除时才值得。对于vector,它不是。

另外,如果你有一个“真正的临时”传递,那么按值传递的值与 sink_of 或“完美”的一样好。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-04-08
    • 2018-03-18
    • 1970-01-01
    • 1970-01-01
    • 2012-07-14
    • 1970-01-01
    • 2011-05-22
    • 2014-03-19
    相关资源
    最近更新 更多