每周技巧 #218:使用 FTADLE 设计扩展点
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #218: Designing Extension Points With FTADLE。
原文最初作为 TotW #218 发布于 2023 年 1 月 19 日。
作者:Andy Soffer
更新于 2023 年 1 月 19 日。
快捷链接:abseil.io/tips/218
Ftadle。这是一个完全 cromulent 的词。~ Unknown
设计扩展点
假设你在开发一个名为 sketchy 的库,它可以在画布上绘图。你已经提供了绘制点、线和文本等常见事物的方式,但你还想提供一种机制,让用户可以指定如何绘制他们自己的类型。你正在设计一个扩展点。
扩展点的设计目标
C++ 提供了许多定义扩展点的机制,每种都有自己的优缺点。在 C++ 中定义扩展点时,有几项考虑值得权衡:
- 可读性:工程师理解你的库与扩展之间关系有多容易?
- 可维护性:随着你的库及其用户需求变化,改变扩展点有多容易?
- 依赖卫生:你的扩展点是否要求把你的库链接进用户的二进制?我们希望扩展点能与 IWYU 良好配合,因此如果某个头文件必须被包含才能让扩展机制工作,被扩展的类型就应当实际使用该头文件中的某些东西。
- 缺少 ODR 违规:某些机制很容易让程序的不同部分对“程序含义”有互相矛盾的看法。ODR 违规永远是 bug。
FTADLE:一个名字很棒的好模式
定义扩展点时,我们建议遵循一种我们亲切称为 FTADLE1(Friend Template ADL Extension,友元模板 ADL 扩展)的模式。FTADLE 在上面列出的每项考虑上都表现不错。它高度依赖一种称为 ADL(Argument Dependent Lookup,实参依赖查找)的语言特性;当函数调用中没有命名空间限定(也就是没有 ::)时,编译器通过这个过程确定意图调用哪个函数。ADL 在技巧 #49 中有详细解释。要使用 FTADLE 模式编写扩展:
- 为扩展点选择一个名称,并加上项目命名空间前缀。我们的扩展用于绘图,项目位于
sketchy命名空间,因此把扩展称为SketchyDraw。 - 设计一个传给
SketchyDraw的类型,让它拥有用户需要的所有行为。在我们的例子中,这就是用户可以在其上绘制类型的sketchy::Canvas。 - 把你的功能实现为一个重载集。这个重载集中的一个成员会是模板,并调用你的扩展点。重载集中的非模板函数应该是基本构建块,也就是你的 API 支持的原始类型。在这个贯穿示例中,这意味着接受
sketchy::Point和sketchy::Line类型的函数。
|
|
有了这个扩展点设计,用户现在就能让自己的类型可绘制。一个无关类型如何添加这种 Draw 功能,而不添加显式依赖?可以使用友元函数来做到。用户在自己的类型中添加一个名为 SketchyDraw、签名合适的友元函数模板,上面的模板重载就会通过 ADL 找到这个 SketchyDraw 函数。例如:
|
|
注意:库用户永远不应直接调用 ADL 扩展点 SketchyDraw。相反,库应提供一个类似 sketchy::Draw 的函数,代表用户调用这个扩展点。
其他例子
FTADLE 模式已经在几个常见库中使用。
AbslHashValue扩展点允许你的类型被 Abseil 的任意哈希容器哈希。细节见技巧 #152。AbslStringify扩展点允许你用许多 Abseil 库打印类型,包括日志、absl::StrCat、absl::StrFormat和absl::Substitute。
应避免什么
一些常见扩展点机制达不到我们的设计目标。虚函数、在编译期检查成员函数、模板特化都各自脆弱,下面逐一讨论。
虚函数
虽然虚函数和类层次结构可能最熟悉,但它们往往过于僵硬。它们几乎无法重构,因为基类和所有派生类都需要同步更新。我们很少第一次就把设计做对,因此拥有一个以后可改变的设计是谨慎的。
除了僵硬之外,类层次结构会强制用户依赖你。在 sketchy 的例子中,即使只有某些二进制想使用这个依赖,用户也必须依赖 sketchy 代码。FTADLE 确保只有需要做 sketchy 相关事情的二进制才付出这个成本。
非模板 friend 扩展点也是如此(例如 std::ostream 的 operator<<)。每个想实现 operator<< 的类都必须包含定义 std::ostream 的某个标准库头文件(例如 <ostream>、<iostream>)。这意味着(不考虑优化时)无论 operator<< 是否被使用,std::ostream 的代码都会被编译并链接进二进制,可能增加编译时间和二进制大小。
成员函数
借助一些模板技巧,你可以检查一个类是否拥有某个特定名称(甚至特定签名)的方法。不过,名称可能会误导。
|
|
使用 FTADLE 模式时,扩展点带有项目命名空间前缀,从而减轻意外符合要求的问题。
模板特化
另一种常见但危险的技术是使用模板特化。std::hash 和 std::less 就是这样被特化的。
|
|
除了需要更多样板代码,这种技术也很容易造成 ODR 违规。虽然并不十分常见,但在不同翻译单元中为同一类型提供不同特化,甚至把同一定义写两次,都是 ODR 违规。更常见的是,如果这种特化只在某些翻译单元可见而在其他翻译单元不可见,元编程技术对“是否有可用哈希函数?”这个问题会产生不同答案,这同样是 ODR 违规。
除此之外,打开一个你不拥有的命名空间通常也是坏实践(原因之一就是会导致 ODR 违规)。我们应该设计 API 来避免坏实践,以免无意鼓励危险做法。
结论
FTADLE 扩展点模式可读、可维护、能缓解 ODR 违规,并避免添加依赖。如果你的库需要扩展点,强烈推荐 FTADLE。