Item 41:理解隐式接口与编译期多态

对齐 Effective C++ 第三版 Item 41:比较 class 的显式接口和运行期多态与 template 的隐式接口和编译期多态,从完整 valid expressions 推导模板约束,并用 concepts 改善契约与诊断。

学习目标

  • 能区分 class client 的 explicit interface/runtime polymorphism 与 template client 的 implicit interface/compile-time polymorphism
  • 能从模板正文逐条推导 T 必须满足的 valid expressions,而不误写成固定成员签名
  • 能设计 concept 与正反 contract tests,把隐式要求变成清晰的编译期边界和诊断

从两种 doProcessing 开始

先看只接受 Widget 的普通函数:

void doProcessing(Widget& value) {
    if (value.size() > 10 && value != someNastyWidget) {
        Widget temp(value);
        temp.normalize();
        temp.swap(value);
    }
}

Widget 的 public declarations 明确告诉客户可调用哪些 names、参数和返回类型。若其中函数是 virtual,运行期会按对象动态类型选择 override。

把函数泛化为 template 后,表面只写了 T

template<class T>
void doProcessing(T& value) {
    if (value.size() > 10 && value != someNastyValue) {
        T temp(value);
        temp.normalize();
        temp.swap(value);
    }
}

Item 41 的原则是 Understand implicit interfaces and compile-time polymorphism(理解隐式接口和编译期多态)。

先预测:满足此模板是否一定要求 T::size() 返回整数?不要看函数名字猜签名,要检查完整 expression。

隐式接口来自 template body

模板参数 T 的约束散布在函数正文:

  • value.size() 必须可调用
  • value.size() > 10 必须形成可转换为条件值的结果
  • value != someNastyValue 必须有效
  • T temp(value) 必须可构造
  • temp.normalize() 必须可调用
  • temp.swap(value) 必须可调用

这些要求合起来就是 doProcessing 对 T 的 implicit interface。它不是某个头文件里的 abstract base,也不要求类型之间存在继承关系。

模板契约实验

先预测:哪一个 valid expression 会决定边界?

切换具体类型,观察 templates 的 implicit interface 如何在实例化时形成 compile-time polymorphism;最后把语法通过与 semantic contract 分开。

template body → expression set → compile-time boundaryWidget共同 base 的显式接口template bodyvalue.size()T temp(value)normalize / swap使用点定义 implicit interface通过size()> 10expression #1通过operator!=sentinelexpression #2通过T temp(value)expression #3通过normalize()body callexpression #4通过swapADL / memberexpression #5观察边界runtime polymorphism:入口接口先写在 class 声明里。client 看到的是 explicit interface每个 expression 仍然要在调用处成立5/5 expressions pass
交互图把 template body 的真实使用点展开为 valid expressions;切换场景即可对比 explicit interface、implicit interface 和 compile-time polymorphism。

当前观察 · Widget

class declarations 描述名称;virtual dispatch 在运行期选行为。

不要把 expression 误缩成固定 signature

value.size() > 10 并不要求 size() 返回 int。例如返回一个 proxy,且该 proxy 与 int 有合适的 operator>,expression 仍然有效。

class CountProxy {
public:
    friend bool operator>(CountProxy count, int threshold);
};
 
class ImageBatch {
public:
    CountProxy size() const;
};

同理,value != someNastyValue 不要求 T 自己有 member operator!=。它可以由 non-member overload、另一个 operand 的 overload 或隐式 conversion 满足。

隐式接口约束的是“这句话能否成立”,不是“必须有一模一样的函数声明”。

compile-time polymorphism 发生在哪里

Widget widget;
ImageBatch batch;
 
doProcessing(widget); // instantiate for Widget
doProcessing(batch);  // instantiate for ImageBatch

每个调用点推导 T,编译器为该 T 解析 sizeoperator!=、construction、normalizeswap。不同类型可选择不同 overload,甚至通过 specialization 采用不同实现。这就是 compile-time polymorphism。

它与 runtime virtual dispatch 的关键差异:选择发生在编译时,通常不要求共同 base 或 vtable;但每个实例可能生成代码,错误也会在实例化时暴露。

鸭子类型不是“随便能跑”

常用口号是“如果像鸭子一样走和叫就可以”,但工程上必须写出精确的 expression set。只测一个 happy-path 类型,会漏掉 const、value category、exception 和返回结果约束。

template<class T>
void inspect(const T& value) {
    auto n = value.size();
    log(n);
}

这里的 receiver 是 const T&,所以非 const size() 不满足;n 还必须可传给可见的 log。这些细节都是 implicit interface 的一部分。

ADL 让非成员定制进入隐式接口

泛型代码常写 unqualified swap(a, b),使 argument-dependent lookup 找到类型命名空间中的定制 overload。

template<class T>
void exchange(T& left, T& right) {
    using std::swap;
    swap(left, right);
}

此时 implicit interface 不是“必须有 member swap”,而是 unqualified swap expression 必须有效。Widget 可提供 friend/non-member swap,普通类型可回退到 std::swap

失败诊断为什么常常很深

没有显式约束时,不合格类型只有在 compiler 实例化到失败 expression 时才报错。

struct Unprocessable {
    int size() const;
    // no normalize, no swap
};
 
// doProcessing(value); // failure inside template body

错误可能指向 temp.normalize(),也可能列出大量 substitution candidates。隐式接口一直存在,只是没有被命名和放在入口处检查。

用 concept 为隐式接口命名

C++20 concepts 可以把同一批 expressions 提前写成可复用 predicate。

template<class T>
concept Processable =
    std::copy_constructible<T> &&
    requires(T value, const T cvalue) {
        { cvalue.size() > 10 } -> std::convertible_to<bool>;
        { value.normalize() };
        { value.swap(value) };
    };
 
template<Processable T>
void doProcessing(T& value) {
    // same algorithm
}

concept 没有把结构化约束变成继承接口;它仍检查 valid expressions。收益是接口文档、overload selection 和 diagnostics 更靠近调用边界。

concept 也不要过度约束

如果算法只忽略 normalize() 的返回值,就不应要求它正好返回 void;如果只需交换,不应要求 member swap 而排除合法 ADL swap。约束应与函数正文真实使用保持一致。

template<class T>
concept SwappableProcessable = requires(T a, T b) {
    { a.normalize() };
    swap(a, b); // requires appropriate lookup setup in real code
};

约束和实现应共享 helper/customization point,避免二者逐渐漂移。

正反类型共同测试 contract

只写一个满足 concept 的类型不能证明边界。至少准备:

  1. 最小合格类型,只实现算法真正需要的 expressions。
  2. proxy-return 类型,证明没有误要求固定返回类型。
  3. const 不合格类型,验证 receiver qualification。
  4. 缺少 normalize 或 swap 的负例,验证 rejection 位置。
  5. 语法合格但语义错误的 fake,验证 runtime/property tests。
static_assert(Processable<Widget>);
static_assert(!Processable<Unprocessable>);

先预测每个 static assertion,再查看 compiler diagnostics;这样读者把 template error 转化为“哪条 expression 不成立”的定位流程。

小结

  • classes 的 explicit interfaces 围绕 declarations,runtime polymorphism 通常来自 virtual functions
  • templates 的 implicit interfaces 围绕必须成立的 complete expressions
  • expression 有效不等于某个 member 必须具有固定 signature,overload 与 conversion 都可能参与
  • template instantiation 和 overload resolution 产生 compile-time polymorphism
  • concepts 为表达式集合命名并改善选择与诊断,但不改变结构化约束本质
  • 编译期合法只证明语法契约,semantic contract 仍需运行测试

资料与写作方式声明

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

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

名词解释

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

explicit interface

由 declarations 直接写出的类型契约。

runtime polymorphism

运行期经 virtual dispatch 选择行为。

implicit interface

由泛型代码有效表达式集合定义的契约。

compile-time polymorphism

实例化和重载阶段确定的静态多态。

valid expression

查找、转换和类型规则都成立的完整表达式。

expression-derived requirement

从模板使用反推的类型要求。

expression proxy

代表结果并参与后续运算的对象。

expression overload resolution

为完整操作选择候选和转换的过程。

template instantiation

以具体参数检查并生成模板实体。

per-type specialization behavior

不同类型得到不同静态操作序列。

structural generic compatibility

依据可用操作而非名义继承的匹配。

receiver qualification requirement

对接收者 const 和值类别的要求。

result-use constraint

结果继续参与表达式形成的连锁约束。

argument-dependent lookup

按实参关联作用域扩展函数查找。

customization point expression

允许用户类型提供定制操作的调用表达式。

instantiation diagnostic stack

模板展开到失败表达式的诊断链。

named concept

参与候选选择的命名编译期约束。

requires expression

只检查表达式及结果约束的语法。

accidental overconstraint

超出算法真实需求的限制。

requirement-use alignment

约束与真实操作保持一致。

minimal conforming type

只满足最小要求的测试类型。

negative contract type

验证拒绝边界的故意不合格类型。

练习

  1. 问题 1:推导 doProcessing 的隐式接口。 不允许写“size 必须返回 int”一类猜测。
  1. 问题 2:把隐式要求写成 concept。 同时支持 proxy size 和 ADL swap,并避免过度约束。
  1. 问题 3:区分编译期合法和语义正确。 某类型的 swap 只交换一半状态,但 concept 通过。

讨论

评论区加载中…