我的一个函数将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);
}
我应该避免哪种方法以及为什么?
答案 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::vector
和std::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::string
和std::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);
}
优点:
缺点:
要点:
只要你没有这样的原型,这是一个很好的选择。
答案 3 :(得分:0)
目前的建议是按值取矢量并将其移动到成员变量中:
void fn(std::vector<std::string> val)
{
m_val = std::move(val);
}
我刚检查过,std::vector
确实提供了一个移动赋值运算符。如果呼叫者不想保留副本,他们可以将其移动到呼叫站点的功能中:fn(std::move(vec));
。