Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 93 additions & 33 deletions book/src/cpp11/01-default-and-delete.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

# default 和 delete - 显式控制特殊成员函数

`= default` 和 `= delete` 是 C++11 引入的两种 **函数定义方式**, 让程序员可以在源码层面显式声明 "这个特殊成员我要编译器生成默认实现" 或 "这个函数禁止被调用", 把原本只能靠编译器隐式规则推断的特殊成员控制权重新交还给设计者
`= default` 和 `= delete` 是 C++11 引入的两种 **函数定义语法**, 用于在源码层面显式指定特殊成员函数采用编译器生成的默认实现, 或显式禁止某一函数参与调用与重载决议. 二者将特殊成员函数的生成控制权从编译器的隐式规则交还给类的设计者

| Book | Video | Code | X |
| --- | --- | --- | --- |
Expand All @@ -17,20 +17,20 @@

**为什么引入?**

- 在 C++11 之前, 只要类里写了任何一个用户定义的构造函数, 编译器就会停止生成默认构造函数, 没有显式办法把它"找回来"
- 没有标准方式表达 "这个特殊成员我故意不要 - 谁调用就编译错". 老办法是 "把拷贝构造声明成 private 且不实现", 错误信息晦涩、还要等到链接期才能暴露
- 不同编译器对 "什么时候自动生成 / 自动删除特殊成员" 的隐式规则容易让人记错, 显式标记可以让意图直接写在代码里
- 在 C++11 之前, 只要类中定义了任意一个用户声明的构造函数, 编译器即停止合成默认构造函数, 且缺乏单独控制某一构造函数是否生成的手段
- 缺乏表达 "禁用某一特殊成员" 的标准语法. 传统做法是将拷贝构造声明为 private 且不提供定义, 其错误信息晦涩, 且需延迟至链接期才能暴露
- 各编译器关于 "特殊成员何时自动生成或自动删除" 的隐式规则存在差异, 易产生记忆偏差; 显式标记可将设计意图直接表达于代码中

**= default 和 = delete 的语义?**

- `= default`: 让编译器按规则生成这个特殊成员的默认实现 (默认构造 / 析构 / 拷贝 / 移动 / [C++20] 比较运算符 等), 等价于 "我要这个生成版本, 别因为我写了别的成员就把它隐式删掉"
- `= delete`: 显式禁止某个函数 - 任何对它的调用 / 重载解析选中它, 都会在 **编译期** 直接报错, 错误信息明确指向 "use of deleted function"
- `= default`: 指示编译器按语言规则合成该特殊成员的默认实现 (默认构造 / 析构 / 拷贝 / 移动 / [C++20] 比较运算符 等), 用于在已声明其他成员的情况下显式保留该成员, 避免其被隐式删除
- `= delete`: 显式禁用某一函数. 任何对其的调用, 或重载决议选中该函数的情形, 均在 **编译期** 报错, 错误信息明确指向 "use of deleted function"

## 一、基础用法和场景

### 显式 default - 把被屏蔽的默认构造要回来
### 显式 default - 恢复被抑制的默认构造函数

只要类里出现任何用户定义的构造函数, 编译器就不再为它合成默认构造. 下面 `B` 的写法就直接禁掉了 `B b;` 这种调用
只要类中出现任意用户声明的构造函数, 编译器即不再为其合成默认构造函数. 下例中 `B` 的定义因此禁用了 `B b;` 形式的调用

```cpp
struct B {
Expand All @@ -41,7 +41,7 @@ B b; // 错误: 没有默认构造函数
B b2(10); // ok
```

`= default` 显式声明一份默认构造, 就能把它要回来, 又不影响已经写好的有参构造
通过 `= default` 显式声明一份默认构造函数即可将其恢复, 同时不影响已声明的有参构造函数

```cpp
struct B {
Expand All @@ -53,12 +53,12 @@ B b; // ok
B b2(10); // ok
```

类似的, `C` 里如果同时有无参构造和带默认值的有参构造, 这两者会在 `C c;` 的重载解析中产生 **二义性**. 把无参版本写成 `= default` 并去掉有参的默认值, 意图就清晰了
类似地, 当 `C` 同时具有无参构造函数与带默认实参的有参构造函数时, 二者在 `C c;` 的重载决议中产生 **二义性**. 将无参版本声明为 `= default` 并移除有参版本的默认实参, 即可消除二义性并明确意图 (或只保留默认参数版本也可)

```cpp
struct C {
C() = default;
C(int x) { std::cout << "C(int x): " << x << std::endl; }
//C() = default;
C(int x = 1) { std::cout << "C(int x): " << x << std::endl; }
};

C c1; // 调用 C()
Expand All @@ -67,7 +67,7 @@ C c2(1); // 调用 C(int)

### 显式 delete - 实现不可拷贝对象

`std::unique_ptr` 最关键的语义就是 "独占所有权 -> 不可拷贝, 但可移动". 自己手写一个简化版, 只要把拷贝相关的两个特殊成员 `= delete` 掉、把移动相关的两个写成 `= default` 即可
`std::unique_ptr` 的核心语义为 "独占所有权 -> 不可拷贝, 但可移动". 实现一个简化版本时, 只需将两个拷贝相关的特殊成员声明为 `= delete`, 并将两个移动相关的特殊成员声明为 `= default`

```cpp
struct UniquePtr {
Expand All @@ -86,7 +86,7 @@ UniquePtr b = a; // 错误: copy ctor 已被 delete
UniquePtr c = std::move(a); // ok: move ctor
```

可以用类型萃取在编译期直接验证语义是否符合预期
可借助类型萃取在编译期验证上述语义是否符合预期

```cpp
static_assert(std::is_copy_constructible<UniquePtr>::value == false, "");
Expand All @@ -97,7 +97,7 @@ static_assert(std::is_move_assignable<UniquePtr>::value == true, "");

### 用 = delete 在重载集中"屏蔽"特定参数类型

`= delete` 不止能用在特殊成员上, 任何普通函数的某个重载都可以删掉. 一个常见模式是 **阻止隐式转换**, 让调用者用 "错误" 的参数类型时直接编译失败
`= delete` 不限于特殊成员, 普通函数的任一重载亦可被删除. 一种常见用法是 **阻止隐式转换**, 使调用方传入非预期的参数类型时直接编译失败

```cpp
void func(int x) {
Expand All @@ -111,25 +111,64 @@ func(1); // ok: 走 int 重载
func(1.1f); // 错误: 调用了 deleted function
```

如果不写这个 deleted 重载, `func(1.1f)` 会悄悄发生 `float -> int` 的窄化转换, 截断掉 `0.1`. `= delete` 把它从重载集中移除后, 错误信息就明确多了: "use of deleted function 'void func(float)'"
若不声明该 deleted 重载, `func(1.1f)` 将隐式发生 `float -> int` 的窄化转换并截断 `0.1`. 通过 `= delete` 将其从重载集中移除后, 错误信息更为明确: "use of deleted function 'void func(float)'"

### default / delete 适用的成员清单

`= default` 可以用于编译器原本就能合成的 "特殊成员函数":
`= default` 仅适用于编译器本可合成的特殊成员函数:

- 默认构造 (无参)
- 析构
- 拷贝构造 / 拷贝赋值
- 移动构造 / 移动赋值
- (C++20) `<=>` 等比较运算符

`= delete` 则没有这种限制 - 任何函数声明 (普通函数、成员函数、模板特化、特殊成员) 都可以写 `= delete`
`= delete` 不受此限制 - 任意函数声明 (普通函数、成员函数、模板特化、特殊成员) 均可声明为 `= delete`

## 二、注意事项
## 二、真实案例 - STL 中的 default/delete

> 前述示例用于说明语法, 而 `= default` / `= delete` 的实际价值在标准库自身的实现中体现得最为直接。下面从仓库内置的 [MSVC STL](https://github.com/mcpp-community/d2mcpp/tree/main/msvc-stl) 中选取两段代码, 分别演示「以 `= delete` 表达不可拷贝」与「`= default` / `= delete` 并用」; `_CONSTEXPR23`、`_Mypair` 等为库内部的宏与命名, 阅读时关注 `= default` / `= delete` 即可

### delete 实现不可拷贝:std::unique_ptr

`std::unique_ptr` 的核心语义为 "独占所有权 -> 不可拷贝、只可移动", 与 `## 一`「显式 delete - 实现不可拷贝对象」中手写的简化 `UniquePtr` 一致 —— 标准库的实际实现采用相同方式: 将两个拷贝成员声明为 `= delete`, 移动成员则保留可用

```cpp
// MSVC STL · msvc-stl/stl/inc/memory (有删节) —— class unique_ptr
// 移动构造 / 移动赋值:保留, 转移所有权(此处为库的模板化定义)
_CONSTEXPR23 unique_ptr(unique_ptr&& _Right) noexcept;
_CONSTEXPR23 unique_ptr& operator=(unique_ptr&& _Right) noexcept;

// 拷贝构造 / 拷贝赋值:= delete, 任何拷贝尝试都在编译期报错
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
```

将拷贝两件套声明为 `= delete`、保留移动两件套可用, 即精确表达出一个 "独占、可移动、不可复制" 的类型。前文手写的 `UniquePtr` 与之完全对应

### default / delete 并用:shared_ptr 的基类 _Ptr_base

`shared_ptr` 和 `weak_ptr` 共用的基类 `_Ptr_base` 同时使用了两种标记:基类自身禁止直接拷贝 (`= delete`), 而默认构造和析构则交由编译器合成 (`= default`)。这是 `## 一`「适用清单」中相应规则的实际体现

```cpp
// MSVC STL · msvc-stl/stl/inc/memory (有删节) —— class _Ptr_base
_Ptr_base(const _Ptr_base&) = delete; // 基类禁止直接拷贝
_Ptr_base& operator=(const _Ptr_base&) = delete;

protected:
constexpr _Ptr_base() noexcept = default; // 默认构造交给编译器
~_Ptr_base() = default; // 析构交给编译器
```

`= delete` 用于禁止不应发生的操作, `= default` 用于显式保留应自动生成的成员 —— 同一类中两种标记各司其职, 将 "允许哪些操作、禁止哪些操作" 完整表达于代码中

> 小结: 不可拷贝、独占所有权、显式声明默认成员等设计, 在标准库内部均依赖本章介绍的 default / delete 工具实现。这也是 C++11 引入二者的核心动机之一

## 三、注意事项

### = default 不一定意味着 "trivial"

写了 `= default` 只是表示 "由编译器生成", 但生成出来的版本是否是 trivial / 是否是 noexcept, 取决于这个类的基类和成员. 例如基类的拷贝构造非 trivial, 那么派生类即使写 `= default`, 它的拷贝构造也是 non-trivial
`= default` 仅表示 "由编译器生成", 而生成版本是否为 trivial、是否为 noexcept, 取决于该类的基类与成员. 例如, 当基类的拷贝构造非 trivial 时, 派生类即使声明 `= default`, 其拷贝构造仍为 non-trivial

```cpp
struct HasString {
Expand All @@ -140,11 +179,11 @@ struct HasString {
static_assert(!std::is_trivially_copy_constructible<HasString>::value, "");
```

如果你的代码依赖 "trivial" 这个属性 (例如 memcpy 拷贝、放入 union), 不要只看到 `= default` 就直接下结论, 用 `std::is_trivially_*` 类型萃取去验证
若代码依赖 "trivial" 属性 (例如以 memcpy 拷贝、置入 union), 不应仅凭 `= default` 即作判断, 而应通过 `std::is_trivially_*` 类型萃取加以验证

### delete 也可以用在普通函数上
### delete 同样适用于普通函数

`= delete` 不是特殊成员的专利. 任何普通函数都可以删掉, 既能用来禁止某个重载, 也能用在函数模板里禁止某些特化
`= delete` 并不限于特殊成员. 任意普通函数均可被删除, 既可用于禁止某一重载, 也可用于在函数模板中禁止特定特化

```cpp
template <typename T>
Expand All @@ -159,22 +198,22 @@ only_int(1); // ok
only_int(1.0); // 错误: 调用 deleted function
```

### 不要把 deleted 函数放成 private
### deleted 函数不应声明为 private

老式的 "禁拷贝" 写法是把 copy ctor / copy assign 声明成 private 且不实现. 这个写法在 C++11 之后已经过时, 应该统一改成 `= delete` (放在 public 区), 原因:
传统的 "禁止拷贝" 写法是将 copy ctor / copy assign 声明为 private 且不提供定义. 该写法在 C++11 之后已不推荐, 应统一改用 `= delete` (置于 public 区), 原因如下:

- `= delete` 在重载解析阶段就报错, 错误信息更明确; private + 未实现要等到链接期才暴露
- 放在 public 让所有访问者拿到的错误信息一致, 不会因为 friend / 同类成员里调用而出现不同的错误形态
- `= delete` 在重载决议阶段即报错, 错误信息更为明确; private + 未定义的方式需延迟至链接期才暴露
- 置于 public 可使所有访问者获得一致的错误信息, 不会因 friend / 同类成员中的调用而产生不同的错误形态

### Rule of 0 / 3 / 5 - 设计类时的指导

`= default` / `= delete` 真正的设计价值, 是配合 **Rule of 0 / 3 / 5**:
`= default` / `= delete` 的核心设计价值在于配合 **Rule of 0 / 3 / 5**:

- **Rule of 0**: 类不直接管理资源, 全靠成员的 RAII (例如 `std::string`, `std::vector`, `std::unique_ptr`) -> 不写任何特殊成员, 让编译器自动合成
- **Rule of 3 (C++98)**: 如果实现了拷贝构造、拷贝赋值、析构中的任何一个, 通常另外两个也要实现
- **Rule of 5 (C++11)**: 引入移动语义后, 把移动构造和移动赋值也加进来 - 一旦你显式定义/删除/默认了其中一个, 最好把全部 5 个都写出来, 避免被编译器的隐式规则坑到
- **Rule of 0**: 类不直接管理资源, 完全依赖成员的 RAII (例如 `std::string`, `std::vector`, `std::unique_ptr`) -> 不声明任何特殊成员, 由编译器自动合成
- **Rule of 3 (C++98)**: 若实现了拷贝构造、拷贝赋值、析构之一, 通常需一并实现另外两个
- **Rule of 5 (C++11)**: 引入移动语义后, 移动构造与移动赋值亦纳入其中 - 一旦显式定义、删除或默认其中之一, 应将全部 5 个成员一并声明, 以避免编译器隐式规则带来的非预期行为

## 、练习代码
## 、练习代码

### 练习代码主题

Expand All @@ -184,15 +223,36 @@ only_int(1.0); // 错误: 调用 deleted function

### 练习代码自动检测命令

<details>
<summary>还没有 d2x?点击展开获取方式</summary>

```bash
# 1. 安装 xlings(Linux / macOS)
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash
# Windows PowerShell:
# irm https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 | iex

# 2. 安装 d2x 并拉取本教程
xlings install d2x -y
d2x install d2mcpp

# 3. 进入项目目录 & 运行检查命令
cd d2mcpp
```

</details>

```
d2x checker default-and-delete
d2x checker default-and-delete-1
d2x checker default-and-delete-2
```

## 、其他
## 、其他

- [交流讨论](https://forum.d2learn.org/category/20)
- [d2mcpp教程仓库](https://github.com/mcpp-community/d2mcpp)
- [mcpp-community 社区](https://github.com/mcpp-community)
- [教程视频列表](https://space.bilibili.com/65858958/lists/5208246)
- [教程支持工具-xlings](https://github.com/openxlings/xlings)
- [教程检测工具-d2x](https://github.com/d2learn/d2x)