彻底搞清楚:右值引用/移动语义/拷贝省略/通用引用/完美转发 —— 以最短的篇幅,介绍常见误解(什么时候要用 move?什么时候不能 move?为什么 move 失败?)和基础知识(为什么右值引用变量是左值?为什么会调用移动构造函数?),一步步解释“为什么/是什么/怎么做”。
写在前面
如果你还不知道 C++ 11 引入的右值引用是什么,可以读读这篇文章,看看有什么 启发;如果你已经对右值引用了如指掌,也可以读读这篇文章,看看有什么 补充。
尽管 C++ 17 标准已经发布了,很多人还不熟悉 C++ 11 的 右值引用/移动语义/拷贝省略/通用引用/完美转发 等概念,甚至对一些细节 有所误解(包括我 [emoji])。
本文将以最短的篇幅,一步步解释 关于右值引用的 为什么/是什么/怎么做。先分享几个我曾经犯过的错误。
误解:返回前,移动局部变量
ES.56: Write std::move() only when you need to explicitly move an object to another scope
std::string base_url = tag->GetBaseUrl();
if (!base_url.empty()) {
UpdateQueryUrl(std::move(base_url) + "&q=" + word_);
}
LOG(INFO) << base_url; // |base_url| may be moved-from
上述代码的问题在于:使用 std::move()
移动局部变量 base_url
,会导致后续代码不能使用该变量;如果使用,会出现 未定义行为 (undefined behavior)(参考:std::basic_string(basic_string&&)
)。
如何检查 移动后使用 (use after move):
- 运行时,在移动构造函数中,将被移动的值设置为无效状态,并在每次使用前检查有效性
- 编译时,使用 Clang 标记对移动语义进行静态检查(参考:Consumed Annotation Checking | Attributes in Clang)
误解:被移动的值不能再使用
C.64: A move operation should move and leave its source in a valid state
很多人认为:被移动的值会进入一个 非法状态 (invalid state),对应的 内存不能再访问。
其实,C++ 标准要求对象 遵守 [sec|移动语义] 移动语义 —— 被移动的对象进入一个 合法但未指定状态 (valid but unspecified state),调用该对象的方法(包括析构函数)不会出现异常,甚至在重新赋值后可以继续使用:
auto p = std::make_unique<int>(1);
auto q = std::move(p);
assert(p == nullptr); // OK: reset to default
p.reset(new int{2}); // or p = std::make_unique<int>(2);
assert(*p == 2); // OK: reset to int*(2
另外,基本类型(例如 int/double
)的移动语义 和拷贝相同:
int i = 1;
int j = std::move(i);
assert(i == j)
误解:移动非引用返回值
F.48: Don’t return std::move(local)
std::unique_ptr<int> foo() {
auto ret = std::make_unique<int>(1);
//...
return std::move(ret); // -> return ret;
}
上述代码的问题在于:没必要使用 std::move()
移动非引用返回值。
C++ 会把 即将离开作用域的 非引用类型的 返回值当成 右值(参考 [sec|值类别 vs 变量类型]),对返回的对象进行 [sec|移动语义] 移动构造(语言标准);如果编译器允许 [sec|拷贝省略] 拷贝省略,还可以省略这一步的构造,直接把 ret
存放到返回值的内存里(编译器优化)。
Never applystd::move()
orstd::forward()
to local objects if they would otherwise be eligible for the return value optimization. —— Scott Meyers, Effective Modern C++
另外,误用 std::move()
会 阻止 编译器的拷贝省略 优化。不过聪明的 Clang 会提示 -Wpessimizing-move/-Wredundant-move 警告。
误解:不移动右值引用参数
F.18: For “will-move-from” parameters, pass by X&& and std::move() the parameter
std::unique_ptr<int> bar(std::unique_ptr<int>&& val) {
//...
return val; // not compile
// -> return std::move/forward(val);
}
上述代码的问题在于:没有对返回值使用 std::move()
(编译器提示 std::unique_ptr(const std::unique_ptr&) = delete
错误)。
If-it-has-a-name Rule:
- Named rvalue references are lvalues.
- Unnamed rvalue references are rvalues.
因为不论 左值引用 还是 右值引用 的变量(或参数)在初始化后,都是左值(参考 [sec|值类别 vs 变量类型]):
- 命名的右值引用 (named rvalue reference) 变量 是 左值&#