要支持移动语义,函数参数应该由unique_ptr,value还是rvalue获取?

时间:2017-10-03 16:09:34

标签: c++ c++11 vector move unique-ptr

我的一个函数将vector作为参数并将其存储为成员变量。我正在使用const引用,如下所述。

class Test {
 public:
  void someFunction(const std::vector<string>& items) {
   m_items = items;
  }

 private:
  std::vector<string> m_items;
};

但是,有时items包含大量字符串,所以我想添加一个支持移动语义的函数(或用新的函数替换它)。

我正在考虑几种方法,但我不确定选择哪种方法。

1)unique_ptr

void someFunction(std::unique_ptr<std::vector<string>> items) {
   // Also, make `m_itmes` std::unique_ptr<std::vector<string>>
   m_items = std::move(items);
}

2)按值传递并移动

void someFunction(std::vector<string> items) {
   m_items = std::move(items);
}

3)rvalue

void someFunction(std::vector<string>&& items) {
   m_items = std::move(items);
}

我应该避免哪种方法以及为什么?

4 个答案:

答案 0 :(得分:30)

除非你有理由让矢量生活在堆上,否则我建议不要使用unique_ptr

无论如何,向量的内部存储都存在于堆上,因此如果使用unique_ptr,则需要2度间接,一个用于取消引用向量的指针,再次取消引用内部存储缓冲区。

因此,我建议使用2或3。

如果你使用选项3(需要一个右值引用),那么当你调用{{{}时,你要求你的类的用户传递一个右值(直接来自临时值,或从左值移动)。 1}}。

从左值移动的要求是繁重的。

如果您的用户想要保留该矢量的副本,他们必须跳过这样做。

someFunction

但是,如果你选择选项2,用户可以决定是否要保留副本 - 选择是他们的

保留副本:

std::vector<string> items = { "1", "2", "3" };
Test t;
std::vector<string> copy = items; // have to copy first
t.someFunction(std::move(items));

不要保留副本:

std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(items); // pass items directly - we keep a copy

答案 1 :(得分:14)

从表面上看,选项2似乎是一个好主意,因为它在单个函数中处理左值和右值。然而,正如Herb Sutter在他的CppCon 2014演讲Back to the Basics! Essentials of Modern C++ Style中所指出的那样,这对于左撇子的常见情况是一种悲观。

如果m_items是&#34;更大&#34;比items,您的原始代码不会为向量分配内存:

// Original code:
void someFunction(const std::vector<string>& items) {
   // If m_items.capacity() >= items.capacity(),
   // there is no allocation.
   // Copying the strings may still require
   // allocations
   m_items = items;
}

std::vector上的复制赋值运算符足够智能,可以重用现有的分配。另一方面,按值获取参数将始终需要进行另一次分配:

// Option 2:
// When passing in an lvalue, we always need to allocate memory and copy over
void someFunction(std::vector<string> items) {
   m_items = std::move(items);
}

简单地说:复制构造和复制分配不一定具有相同的成本。复制分配不比复制构建更有效 - 对std::vectorstd::string 更有效。

Herb指出,最简单的解决方案是添加一个右值超载(基本上是你的选项3):

// You can add `noexcept` here because there will be no allocation‡
void someFunction(std::vector<string>&& items) noexcept {
   m_items = std::move(items);
}

请注意,复制分配优化仅在m_items已存在时才有效,因此按值将参数带到构造函数是完全正确的 - 必须以任一方式执行分配。

TL; DR:选择添加选项3.也就是说,lvalues有一个重载,rvalues有一个重载。选项2强制复制构造而不是复制分配,这可能更昂贵(适用于std::stringstd::vector

†​​如果你想看到显示选项2可能是悲观的基准,at this point in the talk,Herb显示了一些基准

‡如果noexcept的移动分配操作符不是std::vector,我们就不应将其标记为noexcept。如果您使用自定义分配器,请咨询the documentation 根据经验,请注意,如果类型的移动分配为noexcept ,则应仅标记noexcept类似的功能

答案 2 :(得分:6)

这取决于您的使用模式:

选项1

优点:

  • 明确表达责任并从调用者传递给被调用者

缺点:

  • 除非向量已使用unique_ptr包裹,否则不会提高可读性
  • 智能指针通常管理动态分配的对象。因此,您的vector必须成为一个。由于标准库容器是使用内部分配来存储其值的托管对象,这意味着每个这样的向量将有两个动态分配。一个用于唯一ptr + vector对象本身的管理块,另一个用于存储项目。

要点:

如果您使用unique_ptr一直管理此向量,请继续使用它,否则请不要使用它。

选项2

优点:

  • 此选项非常灵活,因为它允许来电者决定是否要保留副本:

    std::vector<std::string> vec { ... };
    Test t;
    t.someFunction(vec); // vec stays a valid copy
    t.someFunction(std::move(vec)); // vec is moved
    
  • 当调用者使用std::move()时,对象仅移动两次(无副本),这是有效的。

缺点:

  • 当调用者不使用std::move()时,总是调用复制构造函数来创建临时对象。如果我们使用void someFunction(const std::vector<std::string> & items)并且我们的m_items已足够大(就容量而言)以容纳items,则作业m_items = items将只是一个复制操作,没有额外的分配。

要点:

如果您事先知道此对象在运行时期间会多次重新 - 设置,并且调用者并不总是使用std::move(),我会避免使用它。否则,这是一个很好的选择,因为它非常灵活,尽管存在问题,但仍然可以根据需求提供用户友好性和更高性能。

选项3

缺点:

  • 此选项强制来电者放弃他的副本。因此,如果他想为自己保留一份副本,他必须编写额外的代码:

    std::vector<std::string> vec { ... };
    Test t;
    t.someFunction(std::vector<std::string>{vec});
    

要点:

这比选项#2灵活性差,因此在大多数情况下我会说低劣。

选项4

考虑到选项2和3的缺点,我认为建议另外一个选择:

void someFunction(const std::vector<int>& items) {
    m_items = items;
}

// AND

void someFunction(std::vector<int>&& items) {
    m_items = std::move(items);
}

优点:

  • 它解决了选项2&amp; 2中​​描述的所有有问题的情况。 3同时享受其优势
  • 来电者决定自己保留一份副本
  • 可针对任何给定方案进行优化

缺点:

要点:

只要你没有这样的原型,这是一个很好的选择。

答案 3 :(得分:0)

目前的建议是按值取矢量并将其移动到成员变量中:

void fn(std::vector<std::string> val)
{
  m_val = std::move(val);
}

我刚检查过,std::vector确实提供了一个移动赋值运算符。如果呼叫者不想保留副本,他们可以将其移动到呼叫站点的功能中:fn(std::move(vec));