【问题标题】:To support move semantics, should function parameters be taken by unique_ptr, by value, or by rvalue?为了支持移动语义,函数参数应该由 unique_ptr、value 还是 rvalue 获取?
【发布时间】:2017-10-03 16:09:34
【问题描述】:

我的一个函数将向量作为参数并将其存储为成员变量。我正在使用对向量的 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) 右值

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

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

【问题讨论】:

  • 这完全取决于您希望该类的用户如何与之交互。如果您想知道他们将向量提供给您的班级,您可以使用 3 或 1。如果您想让他们保留限制您为 2 的向量的副本。
  • 除了 unique_ptr 之外的任何东西。
  • @xaxxon 如果调用者使用 std::move 则不会。那只会将内部缓冲区指针交换到成员,这非常便宜

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


【解决方案1】:

除非你有理由让向量存在于堆上,否则我建议不要使用unique_ptr

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

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

如果您使用选项 3(需要右值引用),则在调用 @987654323 时,您会要求类的用户传递右值(直接从临时值或从左值移动) @。

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

如果您的用户想要保留向量的副本,他们必须跳过铁环才能做到这一点。

std::vector<string> items = { "1", "2", "3" };
Test t;
std::vector<string> copy = items; // have to copy first
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

【讨论】:

  • 如果用户想在案例 3 中保留一份副本,写 t.someFunction(std::vector&lt;string&gt;{items}); 而不是变量和移动会更易读。我认为这与选项 2 一样清晰,甚至更清晰。
  • @xaxxon 仅当用户想要保留自己的副本时。如果没有,他们可以使用std::move,向量将被移动,而不是复制
  • @HeroicKatora 你认为someFunction(std::vector&lt;string&gt;{items})someFunction(items) 更清晰。理解“清晰”概念的有趣方法...
【解决方案2】:

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

如果m_itemsitems“大”,您的原始代码将不会为向量分配内存:

// 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。也就是说,为左值设置一个重载,为右值设置一个重载。选项 2 强制复制 construction 而不是复制 assignment,这可能更昂贵(并且适用于 std::stringstd::vector

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

‡ 如果std::vector 的移动赋值运算符不是noexcept,我们不应该将其标记为noexcept。如果您使用的是自定义分配器,请咨询the documentation
根据经验,只有当类型的移动赋值为 noexcept

时,才应将类似的函数标记为 noexcept

【讨论】:

  • > 如果 m_items 比项目“大”,您的原始代码将不会分配内存:这是不正确的 - 它不会为 vector 分配内存,但它很可能strings分配内存
  • @SteveLorimer 谢谢;我忘了考虑向量持有的是什么。
【解决方案3】:

这取决于您的使用模式:

选项 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&lt;std::string&gt; &amp; 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 和 3 描述的所有问题场景,同时也享受了它们的优势
  • 来电者决定是否为自己保留一份副本
  • 可以针对任何给定场景进行优化

缺点:

总结:

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

【讨论】:

  • 好答案。但我认为您正在寻找的词是“灵活”,而不是“敏捷”。 +1 无论哪种方式。
  • @StoryTeller,“灵活”听起来更好
  • @Daniel Trugman 将 std::move(thevector) 传递给 t.someFunction 意味着它将被移动两次?一次当你通过它并再次在 someFunction() 中?
  • @Zebrafish,我改进了我的答案,并包含了对您问题的答案(两步,无副本)。
【解决方案4】:

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

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

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

【讨论】:

  • “目前的建议是”我认为你不一定是最新的..
猜你喜欢
  • 2016-11-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-05
  • 2012-01-21
  • 2020-09-29
  • 2013-04-07
  • 2014-04-09
相关资源
最近更新 更多