10.Move Semantic

一、lvalues 和 rvalues 简介

  • lvalue(左值):具有持久存储地址的表达式或变量,可以位于赋值符号的左侧。
  • rvalue(右值):代表临时对象或无法标识的值,常出现在赋值符号的右侧,不能被赋值。

1.1 例子

1
2
3
4
5
6
7
8
9
10
11
int val = 2;                 // 2 是 r-value
int *ptr = &val; // &val 是 r-value
vector<int> v1{1, 2, 3}; // 1, 2, 3 是 r-values

auto v4 = v1 + v2; // v1 + v2 是 r-value
auto v5 = v1 += v4; // v1 += v4 的结果是 lvalue

size_t size = v1.size(); // v1.size() 是 r-value
val = static_cast<int>(size);// static_cast<int>(size) 是 r-value

v1[1] = 4 * val; // 4 * val 是 r-value

1.2 生命周期差异

  • lvalue 的生命周期取决于其作用域。
  • rvalue 的生命周期通常在表达式结束时即结束,除非被存储或绑定到引用延长。

1.3 lvalue 和 rvalue 引用

  • lvalue 引用T&):绑定到 lvalues。
  • rvalue 引用T&&):专门绑定到 rvalues。
  • const lvalue 引用const T&):可以绑定到 lvalues 和 rvalues,常用于传递参数。

问题解答:对于 auto&& v4 = v1 + v2;,尽管 v4 绑定了一个 rvalue,v4 本身作为一个引用是 lvalue。

二、移动操作

移动语义允许从临时对象转移资源至新对象,减少资源的不必要复制,从而提高性能。

2.1 特殊成员函数

  • 移动构造函数:从存在的 rvalue 创建新对象。
  • 移动赋值操作符:用存在的 rvalue 替换现有对象。

2.2 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 移动构造函数示例
MyVector(MyVector<T> &&other)
: elems(std::move(other.elems)), logicalSize(other.logicalSize), allocatedSize(other.allocatedSize) {
other.elems = nullptr;
}

// 移动赋值操作符示例
MyVector<T>& operator=(MyVector<T> &&rhs) {
if (this != &rhs) {
delete[] elems;
elems = std::move(rhs.elems);
rhs.elems = nullptr;
}
return *this;
}

注意:拷贝构造函数和拷贝赋值操作符的参数是 const T&,因此它们适用于 lvalues 和 rvalues。但如果你实现了一个接受 T&& 参数的移动构造函数和移动赋值操作符,那么编译器将为 rvalues 优先选择 T&&

三、std::move简介

当执行移动操作时,正确使用 std::move 是实现有效移动语义的关键,因为它确保了资源能够被正确地移动而非拷贝。

3.1 移动操作的常见误区

让我们以 RandomBag 类为例来探讨一个常见的误区。RandomBag 类包含一个类型为 vector<int> 的成员变量 elems

1
2
3
4
5
6
class RandomBag {
public:
RandomBag();
private:
vector<int> elems;
};

如果在移动赋值操作中直接赋值而不使用 std::move,如下所示:

1
2
3
4
5
6
RandomBag &RandomBag::operator=(RandomBag &&rhs) {
if (this != &rhs) {
elems = rhs.elems; // 这实际上是拷贝操作
}
return *this;
}

这里的误区在于,尽管 rhs 是 r-value 引用,但未经 std::move 处理,rhs.elems 在赋值时被视为 l-value,导致执行的是拷贝而非预期的移动操作。

3.2 正确使用 std::move

为了确保资源被移动,我们应该这样做:

1
elems = std::move(rhs.elems);

通过 std::moverhs.elems 被转换为 r-value,触发 vector<int> 的移动赋值操作,从而实现资源的有效移动。重要的是要记住,std::move 本身不移动任何东西;它仅将其参数转换为 r-value,真正的移动操作依赖于移动构造函数或移动赋值操作符。

四、举例:通用的 swap 函数

1
2
3
4
5
6
template <typename T> 
void swap(T &a, T &b) noexcept {
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}

五、规则总结

  • 规则五:如果你显式定义(或删除)了拷贝构造函数、拷贝赋值、移动构造函数、移动赋值或析构函数中的任何一个,你应该为所有五个定义(或删除)。
  • 规则零:如果默认操作符工作正常,则不要定义自定义的特殊成员函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RandomBag
{
public:
RandomBag();
RandomBag(const RandomBag &other) = default;
RandomBag(RandomBag &&other) = default;
RandomBag &operator=(const RandomBag &rhs) = default;
RandomBag &operator=(RandomBag &&rhs) = default;
void add(int value);
int removeRandom();

private:
vector<int> elems;
}

六、示例代码

点击下载示例代码


10.Move Semantic
https://ci-tz.github.io/2024/02/12/10-Move-Semantic/
作者
次天钊
发布于
2024年2月12日
许可协议