Item 6:明确拒绝不想要的编译器生成函数

对齐 Effective C++ 第三版 Item 6:用 private 未定义声明、Uncopyable 基类与现代 deleted functions 拒绝复制或移动,并以类型能力矩阵和负向编译测试验证。

学习目标

  • 能解释 private 未定义 copy、Uncopyable base 与 = delete 在访问控制、诊断阶段和 friend 调用上的差异
  • 能设计值对象、独占所有者、共享所有者、身份对象和 view 的 copy/move 能力矩阵
  • 能实现 deleted copy/move 与 deleted overload,并使用 type traits、concepts 和 compile-fail 测试证明禁止契约
Deletion contract / diagnostic phase map语义决策 → 访问/链接差异 → 调用点禁止 → 负向证据disallow compiler-generated functionscapability matrixvalue / owner / identityprivate copy constructoraccess failurefriend may link-faildeleted functioncall-site diagnosticmember + friend blocked= deletecopy / convertexplicit contractuncopyable旧式能力传播compile-fail负向测试为真forbidden conversion精确 overload 被拒绝container fit语义优先于便利禁止契约要同时证明能力矩阵、诊断阶段和资源不变量
从 private/Uncopyable 迁移到 deleted 时,真正升级的是诊断位置与契约可见性。

禁止契约实验

先预测:错误会在哪个阶段被挡住?

先预测 copy/move 能力和诊断阶段,再切换场景查看负向测试证据。

观察

身份对象和独占资源不应被默认 memberwise copy;uncopyable 是旧式组合手段,但它只通过能力传播间接禁止复制。

决策

先按 value、unique owner、shared owner、view、identity 建立 capability matrix,再决定 copy/move 是 default 还是 delete。

当前场景 · disallow compiler-generated functions / uncopyable

先按 value、unique owner、shared owner、view、identity 建立 capability matrix,再决定 copy/move 是 default 还是 delete。

从“门锁为什么不该被复制”开始

现实中的 HomeForSale、mutex、thread、数据库事务或注册身份通常没有合理复制语义。若类未声明 copy,Item 5 说明编译器可能生成 public memberwise copy。

不能只在文档写“请勿复制”,也不应让复制先发生再在运行时抛异常。

Item 6 的目标是 explicitly disallow compiler-generated functions you do not want(明确拒绝不想要的编译器生成函数)。

原书方法:private 声明但不定义

class HomeForSale {
public:
    HomeForSale() = default;
private:
    HomeForSale(const HomeForSale&);
    HomeForSale& operator=(const HomeForSale&);
};

普通调用方因 private 在编译期失败;member 或 friend 有访问权,调用能编译,却在链接时因没有定义而失败。

分属不同阶段。原书方法有效,但错误可能离调用点更远。

Uncopyable base 把链接错误提前

class Uncopyable {
protected:
    Uncopyable() = default;
    ~Uncopyable() = default;
private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator=(const Uncopyable&);
};
 
class HomeForSale : private Uncopyable {};

编译器尝试生成 HomeForSale copy 时,需要调用基类 copy,但派生无权访问 private,因此在编译期失败。

是组合与继承的自然效果。旧代码常见 boost::noncopyable 也是这一思路。

现代方法:deleted functions

class HomeForSale {
public:
    HomeForSale() = default;
    HomeForSale(const HomeForSale&) = delete;
    HomeForSale& operator=(const HomeForSale&) = delete;
};

通常放 public,调用方、member、friend 都得到“use of deleted function”诊断,文档工具和 type traits 也能直接看到契约。

比 private 更准确表达“不存在合法调用者”。

copy 与 move 分开决定

独占 owner 常禁止 copy 但允许 move:

class FileHandle {
public:
    explicit FileHandle(NativeHandle handle) noexcept : handle_{handle} {}
    ~FileHandle() { close_if_valid(handle_); }
 
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    FileHandle(FileHandle&&) noexcept;
    FileHandle& operator=(FileHandle&&) noexcept;
private:
    NativeHandle handle_{};
};

身份对象则可能 copy/move 都删除:移动 mutex 或被外部注册地址的对象会破坏身份关系。

不要为了能放进 vector 就虚构 move;可改用 unique_ptr<Identity> 保存稳定地址。

值、所有者、view 的能力矩阵

通常 copy/move 都支持。若资源成员使用 string/vector,rule of zero 即可。

删除 copy、支持 noexcept move。

可 copy,但复制语义是共享不是深拷贝,必须明确。

如 span/string_view 常可 copy;禁止 copy 反而降低可用性。

多态基类的复制不是普通 copy

按值复制抽象基类会 slicing。常见设计让基类 copy ctor protected/default 或 deleted,并提供 clone:

class Shape {
public:
    virtual ~Shape() = default;
    virtual std::unique_ptr<Shape> clone() const = 0;
protected:
    Shape() = default;
    Shape(const Shape&) = default;
    Shape& operator=(const Shape&) = default;
};

派生 clone 可以调用自己的 copy constructor。对外禁止基类值复制,对内保留实现能力。

不能仅靠 deleted copy 自动提供 clone 的异常保证,还需逐派生测试。

deleted function 不只用于特殊成员

任何函数都可删除,用于阻止危险隐式转换:

bool isLucky(int number);
bool isLucky(char) = delete;
bool isLucky(bool) = delete;
bool isLucky(double) = delete;

若只保留 int,char/bool/double 会隐式转换;deleted overload 被选中后给出清晰失败。

还可删除模板实例族:

template<class T>
void processPointer(T* pointer);
 
template<>
void processPointer<void>(void*) = delete;

现代 concepts 也能在约束层排除,但 deleted overload 往往提供更具体诊断。

容器需求不会改变类型语义

vector<T> 类型声明不一定立即需要 copy,但某个操作可能需要 move insertable。不要先给 T 增加错误 copy 来满足容器;选择符合语义的容器或间接所有权。

static_assert(!std::copy_constructible<FileHandle>);
static_assert(std::move_constructible<FileHandle>);
 
std::vector<FileHandle> files;
files.emplace_back(open_native("data.bin"));

如果 move 可能抛,vector 的强保证路径可能受限。独占 handle move 通常可 noexcept,因为只交换标量句柄。

禁止契约的编译期测试

traits 检查能力矩阵:

static_assert(!std::is_copy_constructible_v<FileHandle>);
static_assert(!std::is_copy_assignable_v<FileHandle>);
static_assert(std::is_nothrow_move_constructible_v<FileHandle>);
static_assert(std::is_nothrow_move_assignable_v<FileHandle>);

可验证 copy_constructible<T>movable<T> 或自定义 cloneable。traits 不足以证明禁止转换的具体诊断,因此再保留 compile-fail fixture。

先预测:isLucky('a') 在只有 int overload 时会发生什么,加入 deleted char overload 后重载选择如何变化?运行编译测试比较错误位置。

  • 普通调用、member 和 friend 都不能 copy deleted 类型。
  • 派生隐式 copy 因基类 deleted copy 自动不可用。
  • unique owner move 后源为空且可析构,目标独占资源。
  • identity object copy/move traits 全为 false。
  • forbidden conversion 精确在调用点指向 deleted overload。

迁移旧式不可复制代码

先搜索 member/friend 是否偷偷复制;旧代码可能依赖链接失败或从未实例化。改为 = delete 后错误提前暴露。再决定 move:原 C++03 noncopyable 类型在现代不应自动假定可移动,必须根据资源/身份语义选择。

若移除 Uncopyable base 会改变对象布局或 ABI,库边界需要版本策略;内部代码可直接删除继承,减少空基类与继承噪声。

小结

  • 不合理复制应成为编译期类型契约,而不是文档或运行时异常
  • 原书 private undefined copy 对普通调用编译失败,但 member/friend 可能延迟到链接失败
  • Uncopyable base 利用基类能力传播;现代 = delete 直接、统一且诊断更清楚
  • copy 与 move 必须按值、独占、共享、view 或身份语义分别决定
  • deleted functions 还能禁止隐式转换和特定模板调用
  • traits、concepts 与 compile-fail tests 共同证明禁止能力和诊断位置

资料与写作方式声明

本章以Effective C++, Third Edition, Item 6权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

禁止复制契约

让不合法复制表达式在编译期失败的类型约束。

unwanted compiler-generated function

语义不希望使用的编译器生成函数。

private undefined copy

private 声明但不定义复制操作的旧式技术。

access-control failure

因无权访问 private/protected 名称而编译失败。

Uncopyable base

以 private copy 让派生隐式复制失败的基类。

capability propagation

base/member 不可操作使外层特殊成员不可用。

deleted function

以 = delete 声明并在调用选择时失败的函数。

explicit deletion

在声明处明确阻止函数调用的语言机制。

copy capability

从左值创建或覆盖独立对象的能力。

move capability

从右值转移表示并保持源有效的能力。

stable identity semantics

对象地址或外部关系不能安全复制移动的语义。

value object
复制后得到独立等价值的类型。
unique owner

独占资源但可通过 move 转移所有权的类型。

shared owner

复制后共享并共同延长资源生命周期的类型。

view type

不拥有资源、复制便宜但受 owner 生命周期约束的类型。

polymorphic clone

保留动态类型的显式虚拟复制接口。

object slicing

按基类值复制导致派生部分丢失。

forbidden conversion

通过 deleted overload 阻止的危险隐式转换。

deleted template case

对特定模板实参明确删除的调用。

container element requirement

容器操作对元素构造复制移动能力的要求。

compile-fail test

以预期编译失败作为成功条件的负向测试。

expression capability test

用 requires 或 concept 查询表达式是否良构。

noncopyable migration

把旧式不可复制技巧迁移到 deleted 声明的过程。

练习

  1. 问题 1:disallow compiler-generated functions 与 uncopyable。 为配置值、文件句柄和线程会话分别决定 default/copy/move,并解释每个 delete/default。
  1. 问题 2:private copy constructor 与 deleted function。 一个库类 private 继承 Uncopyable,并有 friend 工厂,迁移到 = delete 时要验证什么?
  1. 问题 3:= delete 与 uncopyable。 bool isLucky(int) 意外接受 char、bool、double,设计 deleted overload 与测试。

讨论

评论区加载中…