为了支持移动语义,函数参数应该由unique_ptr、值还是右值获取?
To support move semantics, should function parameters be taken by unique_ptr, by value, or by rvalue?
我的一个函数将向量作为参数并将其存储为成员变量。我正在使用对矢量的常量引用,如下所述。
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);
}
我应该避免哪种方法,为什么?
除非你有理由让向量驻留在堆上,否则我建议不要使用 unique_ptr
vector 的内部存储无论如何都在堆上,所以如果您使用 unique_ptr
,您将需要 2 个间接度,一个是取消引用指向 vector 的指针,另一个是取消引用内部存储缓冲区。
因此,我建议使用 2 或 3。
如果你选择选项 3(需要右值引用),你就是在向你的 class 的用户强加一个要求,即他们传递一个右值(直接来自临时值,或者从左值移动) ), 当调用 someFunction
.
从左值移动的要求很繁重。
如果您的用户想要保留矢量的副本,他们必须克服重重困难才能做到这一点。
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
这取决于您的使用模式:
选项 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);
}
优点:
- 它解决了选项 2 和 3 中描述的所有问题场景,同时也享有它们的优势
- 来电者决定是否自己保留副本
- 可以针对任何给定场景进行优化
缺点:
- 如果该方法接受许多参数都作为 const 引用 and/or 右值引用
的数量
总结:
只要您没有这样的原型,这是一个不错的选择。
目前对此的建议是按值获取向量并将其移动到成员变量中:
void fn(std::vector<std::string> val)
{
m_val = std::move(val);
}
我刚刚检查过,std::vector
确实提供了移动赋值运算符。如果调用者不想保留副本,他们可以将其移动到调用站点的函数中:fn(std::move(vec));
.
从表面上看,选项 2 似乎是个好主意,因为它在单个函数中同时处理左值和右值。然而,正如 Herb Sutter 在他的 CppCon 2014 演讲中指出的那样 Back to the Basics! Essentials of Modern C++ Style,这是对左值常见情况的悲观看法。
如果 m_items
比 items
"bigger",您的原始代码将不会为向量分配内存:
// 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
已经存在时才有效,因此按值将参数传递给 constructors 完全没问题 - 分配必须以任何一种方式执行。
TL;DR: 选择 添加 选项 3。即,对左值进行重载,对右值进行一次重载。选项 2 强制复制 construction 而不是复制 assignment,这可能更昂贵(并且适用于 std::string
和 std::vector
)
† 如果你想看到基准显示选项 2 可能是一个悲观化,at this point in the talk,Herb 显示了一些基准
‡ 如果 std::vector
的移动赋值运算符不是 noexcept
,我们不应该将其标记为 noexcept
。如果您使用的是自定义分配器,请咨询 the documentation。
根据经验,如果类型的移动赋值是 noexcept
,类似的函数应该只标记为 noexcept
我的一个函数将向量作为参数并将其存储为成员变量。我正在使用对矢量的常量引用,如下所述。
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);
}
我应该避免哪种方法,为什么?
除非你有理由让向量驻留在堆上,否则我建议不要使用 unique_ptr
vector 的内部存储无论如何都在堆上,所以如果您使用 unique_ptr
,您将需要 2 个间接度,一个是取消引用指向 vector 的指针,另一个是取消引用内部存储缓冲区。
因此,我建议使用 2 或 3。
如果你选择选项 3(需要右值引用),你就是在向你的 class 的用户强加一个要求,即他们传递一个右值(直接来自临时值,或者从左值移动) ), 当调用 someFunction
.
从左值移动的要求很繁重。
如果您的用户想要保留矢量的副本,他们必须克服重重困难才能做到这一点。
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
这取决于您的使用模式:
选项 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);
}
优点:
- 它解决了选项 2 和 3 中描述的所有问题场景,同时也享有它们的优势
- 来电者决定是否自己保留副本
- 可以针对任何给定场景进行优化
缺点:
- 如果该方法接受许多参数都作为 const 引用 and/or 右值引用
总结:
只要您没有这样的原型,这是一个不错的选择。
目前对此的建议是按值获取向量并将其移动到成员变量中:
void fn(std::vector<std::string> val)
{
m_val = std::move(val);
}
我刚刚检查过,std::vector
确实提供了移动赋值运算符。如果调用者不想保留副本,他们可以将其移动到调用站点的函数中:fn(std::move(vec));
.
从表面上看,选项 2 似乎是个好主意,因为它在单个函数中同时处理左值和右值。然而,正如 Herb Sutter 在他的 CppCon 2014 演讲中指出的那样 Back to the Basics! Essentials of Modern C++ Style,这是对左值常见情况的悲观看法。
如果 m_items
比 items
"bigger",您的原始代码将不会为向量分配内存:
// 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
已经存在时才有效,因此按值将参数传递给 constructors 完全没问题 - 分配必须以任何一种方式执行。
TL;DR: 选择 添加 选项 3。即,对左值进行重载,对右值进行一次重载。选项 2 强制复制 construction 而不是复制 assignment,这可能更昂贵(并且适用于 std::string
和 std::vector
)
† 如果你想看到基准显示选项 2 可能是一个悲观化,at this point in the talk,Herb 显示了一些基准
‡ 如果 std::vector
的移动赋值运算符不是 noexcept
,我们不应该将其标记为 noexcept
。如果您使用的是自定义分配器,请咨询 the documentation。
根据经验,如果类型的移动赋值是 noexcept
noexcept