【问题标题】:Pass-by-value resulting in extra move传值导致额外移动
【发布时间】:2014-04-20 23:09:31
【问题描述】:

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

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

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

然后,如果调用代码想避免复制,它可以通过传递一个右值而不是一个左值。 (例如使用 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 (第一个选项),因为这似乎会导致更少的 move 构造函数调用,但如果可以的话,我想采用 pass-by-value 和复制省略。

【问题讨论】:

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


【解决方案1】:

DataWrapperByValue::data_ 是从DataWrapperByValue::DataWrapperByValue(Data data)s 的争论data 移入的,而d2 是从d2 移入的。

在获得左值的情况下,您得出的通过右值引用与按值版本一起传递的结论会产生最佳性能。然而,这被广泛认为是过早的优化。 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 值。如果通过分析或测试您发现这确实会降低性能,您总是可以在事后轻松添加 pass-by-rvalue-reference。

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

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) } { }
};

这里的构造函数总是做正确的事,这在我的例子中可以看出:http://ideone.com/UsltRA

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

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) } { }
};

请注意,您可以省略 SFINAE 检查,但这会导致一些微妙的问题,例如使用显式构造函数进行隐式转换。同样在不检查参数类型的情况下,转换被推迟到构造函数内部,其中有不同的访问权限、不同的 ADL 等。请参阅实时示例:http://ideone.com/yb4e3Z

【讨论】:

    【解决方案2】:

    DataWrapperByValue 有这个构造函数:

    DataWrapperByValue(Data data);
    

    它按值获取参数,这意味着根据它是左值还是右值,它将调用data 参数的复制或移动构造函数。特别是:如果它是一个左值,它会被复制。如果它是一个右值,它会被移动。

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

    很遗憾,这里不能发生复制省略。如果移动很昂贵并且您想限制它们,您可以允许完美转发,以便至少有一个移动或一个副本:

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

    【讨论】:

    • 你说这里不能发生复制省略。是否可以解释为什么在第三种情况下使用返回值创建右值('prvalue')时复制省略是可能的,但在使用 std::move('xvalue')创建时不能在第二种情况下?
    • @ChrisDrew 因为编译器知道不再需要返回的对象,所以它有权省略移动。对于第二种情况,它不会发生,因为DataWrapperByValue(Data data) 不是移动构造函数或复制构造函数,它只是一个接受Data 对象的构造函数。因此,复制/移动无法省略。
    • 不使用 SFINAE 检查正确的类型可能会导致一些细微的问题。想象一下,如果有人从 int 向 Data 类添加显式构造函数,您可以从 int 甚至是 float 构造 DataWrapperByValue,因为您没有大括号初始化数据_
    • @PorkyBrain 当然这只是一个例子。我不想在当前主题之外跑题。
    • 很公平,在手头的示例中不需要它。
    【解决方案3】:

    我相信这是因为您实际上是在编写此代码。

    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&&) 构造函数创建的。然后在 DataWrapperByValue 的构造函数中调用第二个 move 构造函数。

    【讨论】:

    • 更明确地说:第一步是从d2data,第二步是从datadata_
    • 你的例子不准确,DataWrapperByValue 也没有左值。它根据其值类别移动或复制其参数。
    • @ChrisDrew,调用的是Data的移动构造函数,而不是复制构造函数,所以可以正确。
    • @ChrisDrew,没问题。
    猜你喜欢
    • 2019-11-12
    • 1970-01-01
    • 1970-01-01
    • 2019-09-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多