Item 3:尽可能使用 const
对齐 Effective C++ 第三版 Item 3:把 const 用于指针、迭代器、参数和成员函数契约,区分 bitwise/logical constness,并安全实现 mutable 缓存与 const 重载复用。
学习目标
- 能解释 pointer、pointee、iterator 与 member function 上每个 const 约束的对象和可执行权限
- 能实现 const/non-const 成员重载,比较 bitwise constness 与 logical constness 对可观察状态的要求
- 能设计 mutable 缓存的失效与同步协议,验证 const 接口在并发调用下没有数据竞争
const 权限实验
从类型权限到抽象值
先预测每个 const 层级允许的操作,再切换场景查看编译和运行时证据。
const 规则
先把只读能力写进 pointer、pointee、iterator、parameter 和 return type。
潜在失效
把 pointer const 与 pointee const 混为一谈,会让重指向和写入权限错位。
当前场景 · use const whenever possible
先把只读能力写进 pointer、pointee、iterator、parameter 和 return type。
从“const 把错误提前到编译期”开始
↡由类型系统表达某个名称、对象视图或成员函数不应执行特定修改的约束。让编译器替你拒绝意外写入。它可以修饰全局/局部对象、参数、返回引用、指针本身、所指对象和成员函数。
↡通过类型限制调用方可执行操作集合,而不是依赖注释和约定保持只读。越靠近接口声明,越能减少实现与调用方需要共同记忆的隐含条件。
指针 const:读星号两侧
↡指针所指对象不能经该指针修改,但指针可改指向的类型。与
↡指针自身地址不可改变,但所指对象是否可写由 pointee 类型决定的类型。是不同约束。
char greeting[] = "Hello";
char* p1 = greeting; // pointer 与 pointee 都可变
const char* p2 = greeting; // pointee const
char* const p3 = greeting; // pointer const
const char* const p4 = greeting; // 两者 const可用“const 在星号左边约束 pointee,星号右边约束 pointer”快速阅读;T const* 与 const T* 等价。
iterator 也有两层 const
↡可移动位置并解引用元素的泛化指针,由容器定义有效和失效规则。const_iterator 类似 const T*:可递增迭代器,但不能经它修改元素。const std::vector<T>::iterator 则类似 T* const:迭代器对象本身不能递增,元素仍可能可写。
std::vector<int> values{1, 2, 3};
std::vector<int>::const_iterator read = values.begin();
++read; // 正确
// *read = 4; // 编译失败
const std::vector<int>::iterator fixed = values.begin();
*fixed = 4; // 正确
// ++fixed; // 编译失败接口若只读取范围,优先接收 const range 或 const_iterator,避免算法意外修改元素。
const 参数与返回引用限制能力
↡函数承诺不通过该引用修改调用者对象的参数类型。允许绑定 const 对象和临时量,也避免昂贵复制。
double distance(const Point& left, const Point& right);
const Widget& selected_widget(const Registry& registry, Id id);可保护内部对象,但仍要保证生命周期。不要返回局部对象引用,也不要用 const 掩盖悬空设计。
原书还讨论给值返回加 const 以阻止 (a * b) = c 等怪异操作。现代 C++ 中 const T 值返回常阻碍移动或影响重载,通常让类接口和 ref-qualified 操作符处理误用,而不是给临时值普遍加 const。
条款精神是尽量表达不可修改,不是无差别堆叠 const。 Use const whenever possible(尽可能使用 const)中的 possible 由真实语义决定:只读能力应明确,值返回和移动路径则不应被多余顶层 const 阻塞。
const 成员函数参与重载
↡承诺不修改对象抽象值,并可由 const 对象调用的成员函数。末尾 const 属于函数类型,因此可以与非 const 版本重载。
class TextBlock {
public:
const char& operator[](std::size_t position) const
{
return text_.at(position);
}
char& operator[](std::size_t position)
{
return text_.at(position);
}
private:
std::string text_;
};const TextBlock 得到 const char&,普通对象得到 char&。同一查询接口准确传播写权限。
bitwise constness 的局限
↡const 成员函数不修改对象任何非 mutable 数据位的编译器层面规则。也称 physical constness。编译器主要执行这一规则,但它不保证抽象值真的不变。
class CTextBlock {
public:
char& operator[](std::size_t position) const
{
return pText_[position];
}
private:
char* pText_;
};函数没有修改指针 pText_ 的 bits,却返回可写引用,调用方能通过 const 对象改字符。类型检查通过,逻辑契约被破坏。
更接近接口设计意图。const 不是“成员一位不动”,而是“从该只读视图不能观察到抽象值变化”。
mutable 用于不改变抽象值的缓存
↡允许数据成员在 const 成员函数中修改的说明符,适合不改变抽象值的内部状态。例如缓存字符串长度:
class TextBlock {
public:
std::size_t length() const
{
if (!lengthValid_) {
cachedLength_ = text_.size();
lengthValid_ = true;
}
return cachedLength_;
}
private:
std::string text_;
mutable bool lengthValid_{};
mutable std::size_t cachedLength_{};
};任何修改 text_ 的操作都必须使缓存失效,否则 const 查询返回错误结果。
const 不等于线程安全
↡多个线程无同步访问同一内存位置,且至少一个访问为写入的未定义行为。两个线程首次调用上面的 length() 会同时写 mutable 成员。logical constness 允许缓存变化,但 C++ memory model 不允许数据竞争。
class ThreadSafeTextBlock {
public:
std::size_t length() const
{
std::scoped_lock lock{mutex_};
if (!cachedLength_)
cachedLength_ = text_.size();
return *cachedLength_;
}
private:
std::string text_;
mutable std::mutex mutex_;
mutable std::optional<std::size_t> cachedLength_;
};可能使用 mutex、atomic 或 std::once_flag,选择取决于失效需求。mutex 本身通常也需 mutable,因加锁不改变业务抽象值。
避免 const 与非 const 实现重复
两个 operator[] 若都包含复杂边界检查、日志和懒加载,复制实现会漂移。原书建议让非 const 版本调用 const 正本,再安全移除结果 const。
↡以 const overload 作为读取逻辑唯一实现,非 const overload 委托后恢复写权限的模式。class TextBlock {
public:
const char& operator[](std::size_t position) const
{
return checked_character(position);
}
char& operator[](std::size_t position)
{
return const_cast<char&>(
std::as_const(*this)[position]);
}
};这里安全的前提是非 const overload 的 *this 原本确实非 const。反过来让 const 版本调用非 const 版本并 cast away const,可能让真正 const 对象被修改,不可接受。
现代代码也可把公共读取逻辑抽到私有模板或显式 object parameter,减少 cast;关键是保留一个正本,并证明 const 路径绝不获得写权限。
const 接口审计
↡逐个检查参数、返回别名、成员函数、缓存和全局副作用是否符合只读承诺的过程。先从 public API 开始:只读参数是否为 const reference/span;查询是否标记 const;返回 pointer/reference/iterator 是否传播只读;值返回是否不必要地加顶层 const。再检查实现是否通过别名、全局注册表、日志计数或 mutable 改变可观察行为。
先预测:对 const TextBlock block 调用 block[0]、length() 和两个线程并发 length(),分别写出编译权限、抽象值和 memory model 预期。仅“调用能编译”不是验收。
- 静态断言 const 对象查询结果不可赋值。
- 非 const 对象查询结果可写且保持不变量。
- 修改 text 后缓存失效,下一次读取重新计算。
- ThreadSanitizer 下并发首次读取无 data race。
- const 与非 const 重载对非法索引产生一致诊断。
小结
- const 可约束对象、pointee、pointer、iterator、参数、返回别名与成员函数
- pointer-to-const 和 const pointer 是两层独立能力,const_iterator 也有对应差异
- const member overload 让同一接口向 const/非 const 对象传播只读或可写权限
- 编译器主要执行 bitwise constness,接口设计应满足 logical constness
- mutable 适合派生缓存,但必须有失效和线程同步协议
- 非 const 重载可复用 const 正本,但 const_cast 只在原对象非 const 时安全
名词解释
本章出现的专业名词,用大白话再讲一遍。
- const 契约
由类型系统表达某个视图不应执行修改的约束。
- 编译期只读能力
通过类型限制调用方可执行操作集合的能力。
- pointer-to-const
不能经指针修改 pointee,但可改变指向的类型。
- const pointer
指针地址固定,pointee 写权限由另一层类型决定。
- pointee
指针、引用或迭代器最终访问的目标对象。
- iterator
可移动位置并解引用元素的泛化指针。
- const_iterator
可移动但只能读取元素的容器迭代器。
- const reference parameter
承诺不经引用修改调用者对象的参数。
- const reference return
向调用方提供内部对象的只读别名。
- value return
调用方独立拥有的按值函数结果。
- const member function
可由 const 对象调用且承诺不改抽象值的成员。
- const overload
由对象 const 属性选择读写版本的重载。
- bitwise constness
不修改任何非 mutable 成员位的编译器规则。
- logical constness
不改变调用方可观察抽象值的接口语义。
- mutable
允许成员在 const 函数中修改的说明符。
- logical cache
不改变抽象值的内部派生结果缓存。
- data race
无同步并发访问同一位置且至少一个为写的行为。
- 缓存同步协议
规定缓存初始化、失效和并发访问的约定。
- const implementation reuse
非 const overload 委托 const 正本的复用模式。
- const_cast
显式添加或移除 const 限定的转型。
- const audit
检查接口与实现是否满足只读承诺的过程。
练习
- 问题 1:读懂四种指针(use const whenever possible、pointer-to-const、const pointer)。 对
T*、const T*、T* const、const T* const分别写出重指向和修改对象是否允许。
- 问题 2:修复缓存 const(const member function、logical constness、mutable)。 一个 const
checksum()懒缓存结果,在多线程和对象修改后偶尔错误,设计完整协议。
- 问题 3:复用下标重载(bitwise constness、logical constness、const_cast)。 写出 const/non-const operator[] 的单一实现策略,并证明 const_cast 安全前提。