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

Max*_*eap 16 c++ move vector unique-ptr c++11

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

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

 private:
  std::vector<string> m_items;
};
Run Code Online (Sandbox Code Playgroud)

但是,有时候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);
}
Run Code Online (Sandbox Code Playgroud)

2)按值传递并移动

void someFunction(std::vector<string> items) {
   m_items = std::move(items);
}
Run Code Online (Sandbox Code Playgroud)

3)右值

void someFunction(std::vector<string>&& items) {
   m_items = std::move(items);
}
Run Code Online (Sandbox Code Playgroud)

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

Ste*_*mer 32

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

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

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

如果你选择选项3(需要一个右值引用),你就要求你的类用户在调用时传递一个右值(直接来自临时值,或从左值移动)someFunction.

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

如果您的用户想要保留向量的副本,他们必须跳过箍来这样做.

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

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

保留一份副本:

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

不要保留副本:

std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(std::move(items)); // move items - we don't keep a copy
Run Code Online (Sandbox Code Playgroud)

  • 如果用户想在案例3中保留副本,那么编写`t.someFunction(std :: vector <string> {items});`而不是变量和移动将更具可读性.我认为这比选项2更清楚,如果不是更清楚的话. (3认同)

Jus*_*tin 15

从表面上看,选项2似乎是一个好主意,因为它在单个函数中处理左值和右值.然而,正如Herb Sutter在他的CppCon 2014谈话回归基础知识中所说的那样!现代C++风格的要点,这是对左值的常见情况的悲观.

如果m_items"大于" 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;
}
Run Code Online (Sandbox Code Playgroud)

复制赋值运算符on 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);
}
Run Code Online (Sandbox Code Playgroud)

简单地说:复制构造和复制分配不一定具有相同的成本.复制分配比复制构建更有效 - 它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);
}
Run Code Online (Sandbox Code Playgroud)

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

TL; DR:选择添加选项3.也就是说,对于左值有一个重载,对于右值有一个重载.选项2强制复制构造而不是复制分配,这可能更昂贵(并且用于std::stringstd::vector)

†如果你想看到基准测试显示选项2可能是一个悲观,在谈话的这一点上,Herb显示了一些基准测试

‡我们不应该标明这是noexcept如果std::vector我们继续前进,赋值运算符是不是noexcept.如果您使用自定义分配器,请参阅文档.
根据经验,请注意,只有noexcept类型的移动赋值才能标记类似的函数noexcept

  • >*如果m_items比项目"更大",你的原始代码将不会分配内存*:这是不正确的 - 它不会为`vector`分配内存,但它很可能会**分配内存`字符串` (2认同)

Dan*_*man 7

这取决于您的使用模式:

选项1

优点:

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

缺点:

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

摘要:

如果您始终使用a管理此向量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
    
    Run Code Online (Sandbox Code Playgroud)
  • 当调用者使用std::move()该对象时,只移动两次(无副本),这是有效的.

缺点:

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

摘要:

如果你事先知道该对象将被重新运行时-set很多次,主叫方并不总是使用std::move(),我会避开它.否则,这是一个很好的选择,因为它非常灵活,尽管存在问题,但仍然可以根据需求提供用户友好性和更高性能.

选项3

缺点:

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

    std::vector<std::string> vec { ... };
    Test t;
    t.someFunction(std::vector<std::string>{vec});
    
    Run Code Online (Sandbox Code Playgroud)

摘要:

这比选项#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);
}
Run Code Online (Sandbox Code Playgroud)

优点:

  • 它解决了选项2和3中描述的所有有问题的场景,同时也享受了它们的优势
  • 来电者决定不给自己留一份副本
  • 可针对任何给定方案进行优化

缺点:

摘要:

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