= default 与 = delete¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++11 | 2/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-10-27 |
编译器支持:GCC 4.4+ · Clang 3.0+ · MSVC 18.0+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 用 = delete 显式禁用单例注册表的复制 | ev-spdlog-default-delete-registry-001 |
| 用 = delete 禁止拥有线程的周期任务被复制 | ev-spdlog-default-delete-periodic-worker-002 |
| 用 = default 显式声明可复制且可移动的解析器 | ev-drogon-default-copy-move-multipart-003 |
本站证据页:ev-drogon-default-copy-move-multipart-003 · ev-spdlog-default-delete-periodic-worker-002 · ev-spdlog-default-delete-registry-001
解决什么问题¶
C++ 会隐式生成默认构造、析构、拷贝和移动操作。大多数时候这很方便,但有两种相反的需求:一是希望保留编译器生成的语义,同时在代码里明确写出来,让读者知道这是刻意选择;二是希望彻底禁止某些操作,例如禁止复制持有线程或文件句柄的对象。C++11 之前,“禁止复制”只能把拷贝构造声明为 private 且不定义,错误要等到链接期才暴露,也不适用于普通自由函数。
= default 要求编译器生成默认实现,= delete 把函数声明为“已删除”:调用会在编译期报错,且删除的函数仍参与重载决议。两者都可以用于特殊成员函数和普通函数。标准提案为 N2346。
基础用法¶
在函数声明后写 = default 或 = delete。删除的函数不写函数体。
#include <iostream>
class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
virtual ~NonCopyable() = default;
};
class Point {
public:
Point() = default;
Point(int x, int y) : x_(x), y_(y) {}
Point(const Point&) = default;
Point& operator=(const Point&) = default;
int x() const { return x_; }
int y() const { return y_; }
private:
int x_ = 0;
int y_ = 0;
};
int main() {
Point a(1, 2);
Point b = a;
NonCopyable n;
(void)n;
std::cout << b.x() << ' ' << b.y() << '\n';
}
输出 1 2。Point 显式默认了拷贝操作,NonCopyable 删除拷贝后任何复制尝试都会编译失败。完整版本见 examples/cpp11/default_delete.cpp。
进阶用法¶
特殊成员函数的生成规则很微妙。声明任何拷贝操作会抑制隐式移动操作的生成;声明析构函数也会抑制移动。需要“可拷贝且可移动”时,最稳妥的做法是把五个特殊成员都显式写出:析构、拷贝构造、拷贝赋值、移动构造、移动赋值,按需 = default 或 = delete。
= delete 也可用于普通函数和重载。例如删除某个重载以阻止隐式转换:void log(int) = delete; 之后传 int 会编译失败,而其它重载仍可用。删除的函数参与重载决议,因此它比“私有未定义”更能给出清晰错误,也不会拖到链接期。
= default 可以写在类内,也可以写在类外(仅限特殊成员函数)。类内默认通常是隐式 inline;类外定义要走正常链接规则。虚析构可以写 virtual ~Base() = default;,派生类再用 override = default 明确覆写。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| spdlog | include/spdlog/details/registry.h#L27-L31 |
删除注册表的拷贝构造与赋值 | 全局单例不应被复制,约束交给编译器 |
| spdlog | include/spdlog/details/periodic_worker.h#L41-L43 |
删除持有线程的周期任务拷贝 | 对象拥有 std::thread,复制会重复管理 |
| drogon | lib/inc/drogon/MultiPart.h#L122-L127 |
显式默认拷贝与移动构造 | 默认语义已足够,写出意图并保留移动能力 |
三条证据均为 manual 级:作者复核,尚未引入第二名审核者。三处都在生产路径上,覆盖禁止复制与显式默认两种用途。
陷阱与边界¶
声明拷贝会抑制移动。 只写 = default 的拷贝构造,编译器不会自动生成移动操作;需要移动语义时必须显式默认移动构造与移动赋值。
= default 不等于“一定可复制”。 如果成员本身不可复制,默认的拷贝操作会被定义为删除;错误仍然出现,只是位置更符合直觉。
= delete 必须写在首次声明处。 不能先声明再在类外删除;删除的函数不能有函数体,也不能被调用。
删除的函数仍参与重载决议。 这既是优点也是陷阱:删除某个重载后,原本会选它的调用直接报错,而不会悄悄退到其它重载。设计重载集时要确认这是期望行为。
默认移动不清理源对象。 默认移动构造按成员移动,内置类型成员被复制而不是清零;需要“移后为空”的语义要自己实现。
不要滥用 = delete 表达运行期约束。 删除只影响编译期可见的调用;通过函数指针或类型擦除仍可能绕过,运行期检查不能省。
库联动¶
- 标准库容器与智能指针:
std::unique_ptr、std::mutex、std::thread都删除拷贝,只保留移动。 - RAII 封装:文件句柄、套接字、锁守卫通常删除拷贝并默认移动,明确所有权唯一。
- 多态基类:
virtual ~Base() = default;配合派生类override,是常见的接口写法。 - spdlog / drogon:日志注册表与周期任务用
= delete禁止复制,Web 框架的解析器用= default保留值语义。
横向对比¶
| 方案 | 错误时机 | 参与重载决议 | 可表达移动语义 | 主要限制 |
|---|---|---|---|---|
| 隐式生成 | 视使用而定 | 是 | 是 | 意图不明确,规则易被抑制 |
= default |
编译期 | 是 | 是 | 成员不支持时仍会被删除 |
= delete |
编译期 | 是 | 不适用 | 只阻止编译期可见的调用 |
| 私有未定义(C++11 前) | 链接期 | 是 | 否 | 错误信息差,不适用于自由函数 |
跨标准演进:C++11 通过 N2346 引入 = default 与 = delete;C++20 允许在更多场景使用受约束的默认比较,但特殊成员函数的基本规则未变。