为了支持移动语义,应通过unique_ptr,值或rvalue获取功能参数

To support move semantics, should function parameters be taken by unique_ptr, by value, or by rvalue?

本文关键字:值或 ptr rvalue 参数 功能 获取 unique 支持 移动 语义      更新时间:2023-10-16

我的功能之一将向量作为参数并将其作为成员变量存储。我正在使用如下所述的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);
}

我应该避免使用哪种方法?

除非您有理由将矢量生存在堆上,否则我建议不要使用unique_ptr

无论如何,向量的内部存储都存在于堆上,因此,如果您使用unique_ptr,则需要2度间接,一个将指针放在向量的指针上,然后再解除内部存储缓冲区。

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

如果您使用选项3(需要RVALUE参考),则在呼叫someFunction时,您正在向类用户征求他们通过RVALUE(直接从临时性或从LVALUE移动)的要求。

从LVALUE移动的要求很繁重。

如果您的用户想保留向量的副本,则他们必须跳过篮球才能这样做。

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

在表面上,选项2似乎是一个好主意,因为它在单个功能中处理了lvalues和rvalues。但是,正如Herb Sutter在他的CPPCON 2014 Talk 中回到基础知识的那样!现代C 样式的要点这是对LVALUE的常见情况的悲观。

如果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);
}

简单地说:复制构造和复制分配不一定具有相同的成本。复制作业比复制构造更有效&mdash并不是不可能的。对于std::vectorstd::string

更有效

如草药所指出的那样,最简单的解决方案是添加rvalue超载(基本上是您的选项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强制复制 construction 而不是复制分配,这可能更昂贵(并且适用于std::stringstd::vector

†如果您想查看基准显示该选项2可以是悲观的,那么在谈话中,草药显示一些基准

‡如果std::vector的移动分配操作员不是noexcept,我们不应将其标记为noexcept。如果您使用自定义分配器,请咨询文档。
根据经验,请注意,如果类型的移动分配为 noexcept

,则仅应标记类似功能noexcept

这取决于您的使用模式:

选项1

专利:

  • 责任是明确表达并从呼叫者传递给Callee

cons:

  • 除非向量已经使用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()时,对象仅移动两次(无副本),这是有效的。

cons:

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

摘要:

如果您事先知道此对象将是 RE - 在运行时多次设置,并且呼叫者并不总是使用std::move(),我将避免使用它。否则,这是一个不错的选择,因为它非常灵活,尽管情况有问题,但允许用户友好性和需求较高的性能。

选项3

cons:

  • 此选项迫使呼叫者放弃他的副本。因此,如果他想保留自己的副本,他必须编写其他代码:

    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;的有问题的方案3同时享受他们的优势
  • 呼叫者决定将副本保留给自己是否
  • 可以针对任何给定的方案进行优化

cons:

  • 如果该方法接受许多参数,则作为const参考和/或rvalue引用,原型的数量将成倍增长

摘要:

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

当前的建议是通过价值并将其移至成员变量:

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

我刚刚检查了,std::vector确实提供了一个移动分配操作员。如果呼叫者不想保留副本,则可以将其移至呼叫站点上的函数: fn(std::move(vec));