当你开始更频繁地使用移动语义时,很快就会发现:有时你想触发移动语义,但手头的对象却是左值而非右值。下面这个简单的交换函数恰好说明了这一点:
#include <iostream>
#include <string>
template <typename T>
void mySwapCopy(T& a, T& b)
{
T tmp{ a }; // 调用拷贝构造函数
a = b; // 调用拷贝赋值运算符
b = tmp; // 再次调用拷贝赋值运算符
}
int main()
{
std::string x{ "abc" };
std::string y{ "de" };
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
mySwapCopy(x, y);
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
return 0;
}
mySwapCopy
接收两个 T
类型的对象(此处为 std::string
),通过三次拷贝完成交换。程序输出:
x: abc
y: de
x: de
y: abc
显然,频繁拷贝效率低下;三次深拷贝带来了大量不必要的字符串创建与销毁。
若改用移动语义,仅需三次移动即可完成同样的交换,性能会大幅提升。然而,a
与 b
都是左值引用,默认只能触发拷贝。如何强制使用移动语义?答案就是 std::move
。
std::move
在 C++11 中,标准库提供了 std::move
,其功能是用 static_cast
将实参强制转换为右值引用,从而启用移动语义。
只需包含头文件 <utility>
即可使用。
将上例改写为 mySwapMove
:
#include <iostream>
#include <string>
#include <utility> // for std::move
template <typename T>
void mySwapMove(T& a, T& b)
{
T tmp{ std::move(a) }; // 调用移动构造函数
a = std::move(b); // 调用移动赋值运算符
b = std::move(tmp); // 再次调用移动赋值运算符
}
int main()
{
std::string x{ "abc" };
std::string y{ "de" };
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
mySwapMove(x, y);
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
return 0;
}
输出与之前相同:
x: abc
y: de
x: de
y: abc
但性能显著提升:
tmp
初始化时,std::move(a)
把x
转为右值,触发移动构造;- 后续两次
std::move
触发移动赋值,资源被移动而非复制。
另一示例:向容器插入元素
我们也可以用 std::move
把左值元素移动到容器中,避免拷贝。
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main()
{
std::vector<std::string> v;
std::string str{ "Knock" };
std::cout << "Copying str\n";
v.push_back(str); // 传入左值 -> 拷贝
std::cout << "str: " << str << '\n';
std::cout << "vector: " << v[0] << '\n';
std::cout << "\nMoving str\n";
v.push_back(std::move(str)); // 传入右值 -> 移动
std::cout << "str: " << str << '\n'; // 值已转移,结果不确定
std::cout << "vector: " << v[0] << ' ' << v[1] << '\n';
return 0;
}
作者机器上输出:
Copying str
str: Knock
vector: Knock
Moving str
str:
vector: Knock Knock
- 第一次
push_back
传入左值str
,执行拷贝,str
保持不变。 - 第二次传入
std::move(str)
,触发移动,向量元素“窃取”str
内部资源。 - 注意:移动后
str
处于有效但不确定的状态。
被移动对象的合法状态
当资源从临时对象移出时,其后续状态无关紧要,因为临时对象会立即被销毁。
但对于通过 std::move
转出的左值对象,我们仍需知晓其状态:
- 学派一:主张将对象重置为默认/空状态,使其不再占有资源。
- 学派二:主张仅做最便捷的处理,不强制清空,除非方便。
C++ 标准规定:
“除非另行说明,标准库中被移动后的对象应处于有效但未指明的状态。”
上例中,str
被打印为空串,但这并非强制要求;任何合法字符串(含空串、原串或其他)都可能出现。因此,切勿依赖被移动对象的值;若需继续使用,应先赋予新值。
在 mySwapMove()
中,我们先移出 a
的资源,再移入新资源,期间并未读取 a
的旧值,这是安全的。
何时可安全复用被移动对象?
- 可调用不依赖当前值的函数:如赋值、清除、重置、判空等。
- 避免调用依赖当前值的函数:如
operator[]
、front()
等,因为容器可能已空。
核心洞见
std::move()
向编译器表明:程序员不再需要该对象的当前值。
仅在确实不再需要某左值时使用 std::move
,并在此后不作任何关于其值的假设。
若仍需使用,应先为其赋予新值。
其他应用场景
排序算法
选择排序、冒泡排序等需要频繁交换元素。过去只能拷贝,如今可用std::move
提升效率。智能指针转移所有权
可将一个智能指针所管理的资源转移给另一指针。
相关补充
std::move_if_noexcept()
:若对象的移动构造函数声明为noexcept
,则返回可移动的右值;否则返回可拷贝的左值。
详见 课程《std::move_if_noexcept》。
结论
每当我们希望把左值当作右值对待,以触发移动语义而非拷贝语义时,即可使用 std::move
。