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:读星号两侧

是不同约束。

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 版本重载。

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 的局限

也称 physical constness。编译器主要执行这一规则,但它不保证抽象值真的不变。

class CTextBlock {
public:
    char& operator[](std::size_t position) const
    {
        return pText_[position];
    }
private:
    char* pText_;
};

函数没有修改指针 pText_ 的 bits,却返回可写引用,调用方能通过 const 对象改字符。类型检查通过,逻辑契约被破坏。

更接近接口设计意图。const 不是“成员一位不动”,而是“从该只读视图不能观察到抽象值变化”。

mutable 用于不改变抽象值的缓存

例如缓存字符串长度:

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。

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 时安全

资料与写作方式声明

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

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

名词解释

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

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. 问题 1:读懂四种指针(use const whenever possible、pointer-to-const、const pointer)。T*const T*T* constconst T* const 分别写出重指向和修改对象是否允许。
  1. 问题 2:修复缓存 const(const member function、logical constness、mutable)。 一个 const checksum() 懒缓存结果,在多线程和对象修改后偶尔错误,设计完整协议。
  1. 问题 3:复用下标重载(bitwise constness、logical constness、const_cast)。 写出 const/non-const operator[] 的单一实现策略,并证明 const_cast 安全前提。

讨论

评论区加载中…