C++ 为了支持移动语义,函数参数应该由unique_ptr、value或rvalue获取吗?

C++ 为了支持移动语义,函数参数应该由unique_ptr、value或rvalue获取吗?,c++,c++11,vector,move,unique-ptr,C++,C++11,Vector,Move,Unique Ptr,我的一个函数将向量作为参数,并将其存储为成员变量。我对向量使用常量引用,如下所述 class Test { public: void someFunction(const std::vector<string>& items) { m_items = items; } private: std::vector<string> m_items; }; 2) 按值传递并移动 void someFunction(std::vector<s

我的一个函数将向量作为参数,并将其存储为成员变量。我对向量使用常量引用,如下所述

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

 private:
  std::vector<string> m_items;
};
2) 按值传递并移动

void someFunction(std::vector<string> items) {
   m_items = std::move(items);
}
void someFunction(标准::向量项){
m_项目=标准::移动(项目);
}
3) 右值

void someFunction(std::vector&&items){
m_项目=标准::移动(项目);
}

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

除非您有理由让向量存在于堆上,否则我建议不要使用
unique\u ptr

向量的内部存储仍然存在于堆上,因此如果使用
unique\u 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));
std::vector items={“1”、“2”、“3”};
试验t;
std::vector copy=items;//我必须先复印
t、 someFunction(std::move(items));
但是,如果您选择选项2,用户可以决定是否保留副本,这是他们的选择

保留副本:

std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(items); // pass items directly - we keep a copy
std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(std::move(items)); // move items - we don't keep a copy
std::vector items={“1”、“2”、“3”};
试验t;
t、 某些功能(项目);//直接传递项目-我们保留一份副本
不要保留副本:

std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(items); // pass items directly - we keep a copy
std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(std::move(items)); // move items - we don't keep a copy
std::vector items={“1”、“2”、“3”};
试验t;
t、 someFunction(std::move(items));//移动项目-我们不保留副本

这取决于您的使用模式:

选项1

优点:

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

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

如果您始终使用
唯一\u 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
    
总结:

这不如选项2灵活,因此我认为在大多数情况下都不如选项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);
}
void someFunction(const std::vector&items){
m_项目=项目;
}
//及
void someFunction(std::vector&&items){
m_项目=标准::移动(项目);
}
优点:

  • 它解决了选项2和3中描述的所有问题场景,同时也享受了它们的优势
  • 打电话的人决定是否给自己留一份
  • 可以针对任何给定场景进行优化
缺点:

  • 如果该方法接受多个参数作为常量引用和/或右值引用,则
总结:


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

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

void fn(std::vector<std::string> val)
{
  m_val = std::move(val);
}
void fn(标准::向量值)
{
m_val=std::move(val);
}

我刚刚检查过,
std::vector
确实提供了移动赋值操作符。如果调用方不想保留副本,可以将其移动到调用站点的函数中:
fn(std::move(vec))

从表面上看,选项2似乎是个好主意,因为它在一个函数中处理左值和右值。然而,正如赫伯·萨特(Herb Sutter)在其2014年CppCon演讲中指出的那样,这是对左值这一常见情况的悲观看法。

如果
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;
}
简单地说:复制构造和复制分配不一定有相同的成本。复制赋值不太可能比复制构造更有效-它对于
std::vector
std::string
†更有效

赫伯指出,最简单的解决方案是添加右值重载(基本上是选项3):

//您可以在此处添加'noexcept',因为将不进行分配
void someFunction(std::vector&&items)无例外{
m_项目=标准::移动(项目);
}
请注意,复制分配优化仅在
m_items
已存在时才起作用,因此按值向构造函数获取参数是完全正确的-分配必须以任何方式执行

TL;DR:选择添加选项3。也就是说,一个重载用于左值,一个重载用于右值。选项2强制执行复制构造而不是复制分配,这可能会更昂贵(适用于
std::string
std::vector

†如果您希望看到基准显示选项2可能是悲观的,Herb会显示一些基准

——如果
std::vector
的移动赋值运算符不是
noexcept
,我们就不应该将其标记为
noexcept
。如果您使用的是智能手机,请务必咨询
// 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;
}
// 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);
}
// You can add `noexcept` here because there will be no allocation‡
void someFunction(std::vector<string>&& items) noexcept {
   m_items = std::move(items);
}