按值传递导致额外移动

时间:2014-03-15 15:23:19

标签: c++ c++11 move-semantics copy-elision

我正在尝试理解移动语义和复制/移动省略。

我想要一个包含一些数据的类。我想在构造函数中传递数据,我想拥有数据。

在阅读thisthisthis后,我得到的印象是,如果我想要存储副本,那么在C ++ 11中,按值传递应至少为与任何其他选项一样有效(除了增加代码大小的小问题)。

然后,如果调用代码想要避免复制,则可以通过传递rvalue而不是lvalue。 (例如使用std :: move)

所以我试了一下:

#include <iostream>

struct Data {
  Data()                 { std::cout << "  constructor\n";}
  Data(const Data& data) { std::cout << "  copy constructor\n";} 
  Data(Data&& data)      { std::cout << "  move constructor\n";}
};

struct DataWrapperWithMove {
  Data data_;
  DataWrapperWithMove(Data&& data) : data_(std::move(data)) { }
};

struct DataWrapperByValue {
  Data data_;
  DataWrapperByValue(Data data) : data_(std::move(data)) { }
};

Data
function_returning_data() {
  Data d;
  return d;
}

int main() {
  std::cout << "1. DataWrapperWithMove:\n"; 
  Data d1;
  DataWrapperWithMove a1(std::move(d1));

  std::cout << "2. DataWrapperByValue:\n";  
  Data d2;
  DataWrapperByValue a2(std::move(d2));

  std::cout << "3. RVO:\n";
  DataWrapperByValue a3(function_returning_data());
}

输出:

1. DataWrapperWithMove:
  constructor
  move constructor
2. DataWrapperByValue:
  constructor
  move constructor
  move constructor
3. RVO:
  constructor
  move constructor

我很高兴在这些情况下都没有调用复制构造函数,但为什么在第二种情况下调用了额外的移动构造函数?我想Data的任何体面的移动构造函数都应该很快,但它仍然让我感到困惑。我很想使用pass-by-rvalue-reference(第一个选项),因为这似乎导致一个较少的移动构造函数调用,但我想拥抱pass-by-value并复制elision,如果可以的话。

3 个答案:

答案 0 :(得分:4)

DataWrapperByValue::data_已移出DataWrapperByValue::DataWrapperByValue(Data data)data移入的d2争论struct DataWrapperByMoveOrCopy { Data data_; template<typename T, typename = typename std::enable_if< //SFINAE check to make sure of correct type std::is_same<typename std::decay<T>::type, Data>::value >::type > DataWrapperByMoveOrCopy(T&& data) : data_{ std::forward<T>(data) } { } };

对于获得l值的情况,你通过rvalue-reference和by值版本得出最佳性能的结论。然而,这被广泛认为是过早优化。 Howard Hinnant(Best way to write constructor of a class who holds a STL container in C++11)和Sean Parent(http://channel9.msdn.com/Events/GoingNative/2013/Inheritance-Is-The-Base-Class-of-Evil)都注意到他们认为这种过早的优化。原因是移动应该是非常便宜的,并且在这种情况下避免它们会导致代码重复,特别是如果你有多个争论可以是r或l值。如果通过分析或测试发现这个实际确实降低了性能,您可以随时轻松地添加r值 - 参考值。

在您需要额外性能的情况下,有用的模式是:

struct CompositeWrapperByMoveOrCopy {
  Data data_;
  Foo foo_;
  Bar bar_;
  Baz baz_;
  template<typename T, typename U, typename V, typename W, 
    typename = typename std::enable_if<
        std::is_same<typename std::decay<T>::type, Data>::value &&
        std::is_same<typename std::decay<U>::type, Foo>::value &&
        std::is_same<typename std::decay<V>::type, Bar>::value &&
        std::is_same<typename std::decay<W>::type, Baz>::value
    >::type
  >
  CompositeWrapperByMoveOrCopy(T&& data, U&& foo, V&& bar, W&& baz) : 
  data_{ std::forward<T>(data) },
  foo_{ std::forward<U>(foo) },
  bar_{ std::forward<V>(bar) },
  baz_{ std::forward<W>(baz) } { }
};

这里构造函数始终做正确的事情,如我的实例中所示:http://ideone.com/UsltRA

这个可疑的复杂代码的优点可能与单个争论无关,但想象一下如果你的构造函数有4个可能是r或l值的争论,这比编写16个不同的构造函数要好得多。

{{1}}

请注意,您可以省略SFINAE检查,但这允许使用显式构造函数隐式转换等细微问题。在没有检查参数类型的情况下,转换也会延迟到构造函数中,其中存在不同的访问权限,不同的ADL等。请参阅实例:http://ideone.com/yb4e3Z

答案 1 :(得分:3)

DataWrapperByValue有这个构造函数:

DataWrapperByValue(Data data);

它通过值获取其参数,这意味着它将取决于它是左值还是右值,它将调用data参数的副本或移动构造函数。特别是:如果它是左值,则复制它。如果它是一个右值,它就会被移动。

由于您通过std::move(d2)传入右值,因此调用移动构造函数将d2移动到参数中。第二步构造函数调用当然是通过data_数据成员的初始化来实现的。

不幸的是,复制省略不能在这里出现。如果移动费用昂贵并且您想限制它们,您可以允许完美转发,因此至少一个移动或一个副本:

template<class U>
DataWrapperByValue(U&& u) : data_(std::forward<U>(u)) { }

答案 2 :(得分:0)

我相信这是因为你基本上是在做这个代码。

std::cout << "2. DataWrapperByValue:\n";  
Data d2;
DataWrapperByValue a2(Data(std::move(d2))); // Notice a Data object is constructed. 

注意DataWrapperByValue只有一个接受左值的构造函数。当你执行std :: move(d2)时,你传递的是一个r值,因此将创建另一个Data对象以传递给DataWrapperByValue构造函数。这个是使用Data(Data&amp;&amp;)构造函数创建的。然后在DataWrapperByValue的构造函数中调用第二个移动构造函数。