为了支持移动语义,应通过unique_ptr,值或rvalue获取功能参数
To support move semantics, should function parameters be taken by unique_ptr, by value, or by rvalue?
我的功能之一将向量作为参数并将其作为成员变量存储。我正在使用如下所述的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_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
上的复制分配操作员足够聪明,可以重复使用现有的分配。另一方面,按值进行参数始终必须进行另一个分配:
// 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::vector
和std::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::string
和std::vector
)
†如果您想查看基准显示该选项2可以是悲观的,那么在谈话中,草药显示一些基准
‡如果std::vector
的移动分配操作员不是noexcept
,我们不应将其标记为noexcept
。如果您使用自定义分配器,请咨询文档。
根据经验,请注意,如果类型的移动分配为 noexcept
noexcept
这取决于您的使用模式:
选项1
专利:
- 责任是明确表达并从呼叫者传递给Callee
cons:
- 除非向量已经使用
unique_ptr
包装,否则这不会提高可读性 - 一般管理动态分配的对象中的智能指针。因此,您的
vector
必须成为一个。由于标准库容器是使用内部分配来存储其值的托管对象,因此这意味着每个此类向量都会有两个动态分配。一个用于唯一ptrvector
对象本身的管理块,而一个用于存储项目的额外。
摘要:
如果您始终使用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));
。
- 检查类是否具有模板专用化(使用布尔值或 int 等模板参数)
- C++在赋值或回调函数时重载模板
- 存储函数返回值或立即使用c++
- 派生值或附加属性的方法
- 结构中的默认成员值或默认构造函数参数
- 什么 & 返回左值或右值?
- 如何使用非类型参数传递模板化类的 Ref 或 Ptr
- 如何在给定数组的任何子数组(任何大小)中找到最大值(或最小值)?
- 复制堆栈上的成员值或使用指针访问它?
- 一个概念需要 constexpr 值或函数吗?
- std::原子布尔值或普通全局布尔值在单线程中很好吗?
- C - 最好将枚举类作为值或const引用
- 数组是如何通过引用、值或指针传递C++的?
- C++ 中的赋值或增量运算符
- C++(E0158 表达式必须是左值或函数指示符)和(错误 C2102 '&' 需要 l 值)
- QSlider 事件更改最小值、最大值或范围
- put_money是否按值或引用保存其参数
- 从指针访问值或获取其默认值(如果为 null)的最干净方法
- 将布尔值或浮点数分配给int32_t是否安全?
- 我应该通过引用、值或ptr来存储一个完全封装的成员吗