std::move

当你开始更频繁地使用移动语义时,很快就会发现:有时你想触发移动语义,但手头的对象却是左值而非右值。下面这个简单的交换函数恰好说明了这一点:

#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

显然,频繁拷贝效率低下;三次深拷贝带来了大量不必要的字符串创建与销毁。
若改用移动语义,仅需三次移动即可完成同样的交换,性能会大幅提升。然而,ab 都是左值引用,默认只能触发拷贝。如何强制使用移动语义?答案就是 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,并在此后不作任何关于其值的假设
若仍需使用,应先为其赋予新值。


其他应用场景

  1. 排序算法
    选择排序、冒泡排序等需要频繁交换元素。过去只能拷贝,如今可用 std::move 提升效率。

  2. 智能指针转移所有权
    可将一个智能指针所管理的资源转移给另一指针。


相关补充

  • std::move_if_noexcept():若对象的移动构造函数声明为 noexcept,则返回可移动的右值;否则返回可拷贝的左值。
    详见 课程《std::move_if_noexcept》。

结论

每当我们希望把左值当作右值对待,以触发移动语义而非拷贝语义时,即可使用 std::move

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"