C++移动语义与可选值处理
立即解锁
发布时间: 2025-08-20 01:47:47 阅读量: 2 订阅数: 3 


C++高性能编程:从入门到精通
### C++ 移动语义与可选值处理
#### 1. 无移动语义时避免复制
在没有移动语义的情况下,通常通过指针分配对象并传递指针而非实际对象来避免复制问题。例如:
```cpp
auto make_buffer() -> Buffer* {
auto buffer = new Buffer({2.0, 4.0, 6.0, 8.0});
return buffer;
}
auto buffer = make_buffer(); // buffer 是 Buffer*
```
这种方式存在一些缺点:
- 失去了 C++ 值语义的优势,程序员仍需手动处理指针。
- 代码中充斥着仅用于优化的指针,变得臃肿。
- 需要更多的堆分配,可能因缓存未命中和分配增加导致代码变慢。
#### 2. 引入移动语义
为解决上述问题,“三法则”扩展为“五法则”。除了拷贝构造函数和拷贝赋值运算符,现在还有移动构造函数和移动赋值运算符。移动版本接受 `Buffer&&` 对象,`&&` 修饰符表示该参数是一个要进行移动操作而非复制的对象,在 C++ 中这被称为右值(r-value)。
以下是 `Buffer` 类添加移动构造函数和移动赋值运算符的示例:
```cpp
class Buffer {
...
Buffer(Buffer&& other) noexcept
: ptr_{other.ptr_}
, size_{other.size_} {
other.ptr_ = nullptr;
other.size_ = 0;
}
auto& operator=(Buffer&& other) noexcept {
ptr_ = other.ptr_;
size_ = other.size_;
other.ptr_ = nullptr;
other.size_ = 0;
return *this;
}
...
};
```
当编译器检测到看似是复制操作,但被复制的值不再使用时,会使用无异常的移动构造函数或移动赋值运算符,而不是进行复制。注意要将移动构造函数和移动赋值运算符标记为 `noexcept`,否则 STL 容器和算法在某些情况下会使用常规的复制/赋值操作。
#### 3. 命名变量与右值
编译器在对象可被归类为右值时会进行移动操作。右值本质上是未与命名变量绑定的对象,主要有以下两种情况:
- 直接从函数返回的对象。
- 使用 `std::move(...)` 将变量转换为右值。
以下是示例:
```cpp
auto x = make_buffer(); // 从函数返回的对象未绑定到变量,因此移动到 x
auto y = std::move(x); // x 被转换为右值,移动赋值给 y
```
下面通过 `Bird` 类的 `set_song` 方法进一步说明:
```cpp
class Bird {
public:
Bird() {}
auto set_song(const std::string& s) { song_ = s; }
auto set_song(std::string&& s) { song_ = std::move(s); }
std::string song_;
};
auto bird = Bird{};
```
不同调用情况如下:
| 情况 | 代码示例 | 操作类型 |
| ---- | ---- | ---- |
| 情况 1 | `auto cuckoo_a = std::string{"I'm a Cuckoo"}; bird.set_song(cuckoo_a);` | 拷贝赋值 |
| 情况 2 | `auto cuckoo_b = std::string{"I'm a Cuckoo"}; bird.set_song(std::move(cuckoo_b));` | 移动赋值 |
| 情况 3 | `auto make_roast_song() { return std::string{"I'm a Roast"}; } bird.set_song(make_roast_song());` | 移动赋值 |
| 情况 4 | `auto roast_song_a = make_roast_song(); bird.set_song(roast_song_a);` | 拷贝赋值 |
| 情况 5 | `const auto roast_song_b = make_roast_song(); bird.set_song(std::move(roast_song_b));` | 拷贝赋值 |
#### 4. 适用时按移动接受参数
考虑将 `std::string` 转换为小写的函数,原本可能需要两个函数分别处理不同情况:
```cpp
auto str_to_lower(const std::string& s) -> std::string {
auto clone = s;
for(auto& c: clone) c = std::tolower(c);
return clone;
}
auto str_to_lower(std::string&& s) -> std::string {
for(auto& c: s) c = std::tolower(c);
return s;
}
```
但通过按值接受 `std::string`,可以用一个函数处理两种情况:
```cpp
auto str_to_lower(std::string s) -> std::string {
for(auto& c: s)
c = std::tolower(c);
return s;
}
```
当传递常规变量时,函数调用前 `str` 的内容会被拷贝构造到 `s` 中,函数返回时再移动赋值回 `str`;当传递右值时,`str` 的内容在函数调用前会被移动构造到 `s` 中,函数返回时再移动赋值回 `str`,避免了不必要的复制。
#### 5. 默认移动语义与零法则
编译器可以生成移动构造函数和移动赋值运算符,我们可以使用 `default` 关键字强制编译器生成它们。例如 `Bird` 类:
```cpp
class Bird {
...
// 拷贝构造函数/拷贝赋值运算符
Bird(const Bird&) = default;
auto operator=(const Bird&) -> Bird& = default;
// 移动构造函数/移动赋值运算符
Bird(Bird&&) noexcept = default;
auto operator=(Bird&&) noexcept -> Bird& = default;
// 析构函数
~Bird() = default;
...
};
```
如果不声明任何自定义的拷贝构造函数、拷贝赋值运算符或析构函数,移动构造函数和移动赋值运算符会被隐式声明。但如果添加了自定义析构函数,移动运算符将不会被生成,类将始终进行复制操作。
在实际代码库中,需要手动编写拷贝/移动构造函数和拷贝/移动赋值运算符的情况应该很少。编写不需要显式编写这些函数的类通常被称为“零法则”。如果应用代码库中的类需要显式编写这些函数,这些代码可能更适合放在库部分。
#### 6. 空析构函数的注意事项
0
0
复制全文
相关推荐










