C++ 的类型继承设计允许出现继承、多继承和虚拟继承,多种不同的继承关系能够用于描述一些高度复杂的继承关系结构;但这种强大的抽象能力也需要付出相应代价,比如
- 继承会引入包括但不限于虚表和虚基类指针
- 多继承结构极易引发菱形继承问题
- 虚继承会产生“构造函数顺序灾难”问题
在虚继承场景下,一些经典的设计模式,如 CRTP,还会因为基类实例的不确定而无法使用。
但是这种继承机制又是表达复杂类关系所不可或缺的工具,那么有没有什么办法可以同时拿到继承的抽象表达能力,同时不引入它所带来的问题?
这种时候我们可以考虑将复杂的继承结构线性化。
1.继承关系图
C++ 中的继承关系可以被分为虚继承(Virtual Inheritance)和非虚继承(Non-virtual Inheritance);如果我们将类视作顶点,继承关系视作边,并使用虚实线区分两种继承关系,那么就能发现:任意多个类的继承关系天然构成一个无环图。
例如下图所示的一种典型菱形继承情况:
这个无环图可以经拓扑排序为若干个顶点列表:
读者可以发现:我们能够使用 G 继承 C、C 继承 B,B 再继承 A 的方式,将一个菱形继承关系“展平”为一个单一的继承链条。
此时的代码会长这样:
struct A {};
struct B : A {};
struct C : B {};
struct G : C {};
显而易见的是,在常规多继承场景中,一个基类能被多个不同派生类继承;而这种手动排序的方式只能满足部分固定的继承关系,并不能适应所有继承关系。
为了解决这个问题,我们需要将派生类模板化,从外部注入基类依赖。
// 所有基类都模板化了,需要有一个额外的非模板类型作为第一个基类
// 才能填充模板参数列表
struct Nil {};
template<typename Base>
struct A : Base {};
template<typename Base>
struct B : Base {};
template<typename Base>
struct C : Base {};
struct G : C<B<A<Nil>>> {};
由于继承链上每个基类都是它们基类的派生类,所以除了第一个基类之外的所有基类,都会受到这个规则的影响变成模板类;这个时候最派生类的继承列表就会显得过于嵌套。
为此我们可以引入一个辅助类型,这个辅助类型能够根据给定的拓扑序自动展开继承列表,并返回得到的直接基类类型。
因为根据前文的要求,继承图中除了第一个基类外的所有顶点都是 C++ 的模板类,所以这个辅助类型所要处理的类型是一种特殊的模板类型:模板模板类型;模板类的模板模板类型不属于普通的模板类型,它不能被传递给标准库的类型函数。
这很拗口。
#include <type_traits>
template<typename Base>
struct B : Base {};
template<typename Base>
struct C : Base {};
// 我们不能使用 std::is_same 判断两个模板类是否相等
std::is_same<B, C>::value; // ERROR!
但是这种类型在参数传递上是与普通模板类型一致的,除了声明时相对繁琐了一点外并没有什么不同。为了能让模板函数在递归调用时传递一组已排序类型,我们还要引入一个能够容纳模板模板类型的类型容器。
// 用于存储模板类的类型容器
template<template<typename...> class... Ts> // 这就是模板模板类型的模板形参
struct TemplateList;
using List = TemplateList<B, C>; // 像这样使用
可能读者会疑惑为什么这个容器的参数类型是 template<typename...> class
而非 template<typename> class
,这是因为模板类的模板参数列表可以是任意多的;例如模板类 std::vector
就持有一个除了数据类型之外的内存分配器类型。
这里使用 template<typename...> class
能够匹配任意数量的模板参数列表。
理所当然的,因为可能存在有模板类会有额外模板参数需求,所以我们的辅助类型还需要提供这样的额外参数接口,用于在展开拓扑排序列表的同时自动填充模板参数。
C++ 的模板元编程完全遵从函数式编程范式,因此之后的代码中只会存在模板特化组成的模式匹配及条件分支,和自我调用的递归操作。
这个过程写起来挺复杂的:
// LI 是什么意思后面再说
template<template<typename...> class Base, template<typename...> class... Bases>
struct LI {
private:
template<typename TopoOrder, typename RBC, typename... Args>
struct Helper;
template<typename TopoOrder, typename RBC, typename... Args>
using Helper_t = typename Helper<TopoOrder, RBC, Args...>::type;
template<typename RBC, typename... Args>
struct Helper<TemplateList<>, RBC, Args...> {
using type = RBC;
};
template<template<typename...> class Head,
template<typename...> class... Tail,
typename RBC,
typename... Args>
struct Helper<TemplateList<Head, Tail...>, RBC, Args...> {
using type = Head<Helper_t<TemplateList<Tail...>, RBC, Args...>, Args...>;
};
public:
// RBC: Root Base Class
template<typename RBC, typename... Args>
using type = Helper_t<TemplateList<Base, Bases...>, RBC, Args...>;
};
现在可以在一定程度上简化类的继承声明:
struct G : LI<C, B, A>::template type<Nil> {};
不过类继承是有依赖性的,现在我们只能手动排序拓扑关系图,并写出完整的依赖链;如果某个基类的依赖关系发生了改变,那么就需要去修改所有有关的派生类。
所以我们需要一个排序算法。
2.线性化算法
不难发现,我们对继承关系图做拓扑排序,实际上是在将一个复杂的继承图结构展开为一条线性列表,这也可以被称作是“类的线性化算法”。
所以
LI
的意思就是Linearized Inheritance
。
在 Python 等支持多继承语法的语言中,使用了一个名为 C3 线性化算法的操作解析类继承关系;但与 C++ 不同的是,这类语言解析的是查找类方法的遍历顺序,而我们想解析是基类的继承顺序。
Python 通过
super()
和魔术方法,以及使用 C3 线性化的 MRO(Method Resolution Order,方法解析顺序)解决了菱形继承中重复的类实例问题。
实际上,我们只需要一个朴素的拓扑排序算法就足以解析所有无环继承关系图;不过拓扑排序只是一类算法,它不规定排序策略和结果要求,因此 C3 线性化算法依然有一些东西值得我们参考:
- Python 在使用 C3 线性化之前使用的是深度优先策略
- C3 线性化明确规定了最终返回的解析顺序
在对无环图排序的过程中,我们可以简单地将拓扑排序视作是从某个边界顶点出发,按照深度优先的顺序遍历整个图;此时访问顺序本身就是一个拓扑排序结果。
因为继承结构中只有第一个基类和最后一个派生类(最派生类)属于一个继承关系图的“边界”(也就是说继承关系从第一个基类开始,再到最派生类结束);而且我们也只能在最派生类处获取该派生类所依赖的基类,进而生成待排序的继承图,所以只需要实现一个简单地、编译期执行深度优先搜索的排序算法即可。
非常凑巧的是,C++ 的模板系统是图灵完备的;我们可以在编译期就使用模板和特化技术完成两个基本的控制流:分支和递归,所以在编译期深度优先遍历一个图并不是不可能的事。
我们还需要自行设计一套编译期类型容器(也就是前文的 TemplateList
),以及与之交互的函数。我们只需要实现拓扑排序,所以以下几个基本组件就足够了:
#include <type_traits>
// 适用于模板模板类型的 std::is_same
template<template<typename...> class T, template<typename...> class U>
struct Equal : std::false_type {};
template<template<typename...> class T>
struct Equal<T, T> : std::true_type {};
// 向给定的 TemplateList 首部插入一个模板类
template<typename List, template<typename...> class T>
struct Prepend;
template<template<typename...> class... Ts, template<typename...> class T>
struct Prepend<TemplateList<Ts...>, T> {
using type = TemplateList<T, Ts...>;
};
// 类型别名,用于简化使用
template<typename List, template<typename...> class T>
using Prepend_t = typename Prepend<List, T>::type;
// 检查某个模板类是否被包含在给定的 TemplateList 中
template<typename List, template<typename...> class T>
struct Contain;
template<template<typename...> class T>
struct Contain<TemplateList<>, T> : std::false_type {};
template<template<typename...> class Head, template<typename...> class... Tail, template<typename...> class T>
struct Contain<TemplateList<Head, Tail...>, T>
: std::conditional<Equal<Head, T>::value, std::true_type, Contain<TemplateList<Tail...>, T>>::type {};
继承关系不能无中生有,我们还需要有一个辅助类型以记录每个类的依赖关系;这个“记录”被表示为该辅助类型在某一类型上的特化版本。
template<template<typename...> class Node>
struct InheritFrom {
using type = TemplateList<>;
};
template<template<typename...> class Node>
using InheritFrom_t = typename InheritFrom<Node>::type;
// 提供一个简化特化操作的辅助宏
#define InheritanceRegister( Node, ... ) \
template<> \
struct InheritFrom<Node> { \
using type = TemplateList<__VA_ARGS__>; \
}
因为模板元编程是纯粹函数式的,所以不存在一种能够记录继承关系的全局数据结构,在这种情况下,所有状态都被困在了一个固定的调用上下文中。
如果想有创建适用于模板元编程的全局带状态数据结构,请参阅状态元编程 STMP。
之后只需要写出从派生类出发,递归解析基类、并执行拓扑排序的深度优先搜索函数;解析策略很简单:判断当前类型是否在已排序列表中,是则跳过当前排序类型,否则将当前类型加入到列表中。其余操作均为实现细节,并且深度优先策略有多种实现方式,这里采用的是后序遍历:
template<template<typename...> class Base, template<typename...> class... Bases>
struct TopoSort {
private:
template<typename List, typename SortedList>
struct Helper;
template<typename List, typename SortedList>
using Helper_t = typename Helper<List, SortedList>::type;
template<typename SortedList>
struct Helper<TemplateList<>, SortedList> {
using type = SortedList;
};
template<template<typename...> class Head, template<typename...> class... Tail, typename SortedList>
struct Helper<TemplateList<Head, Tail...>, SortedList> {
using type =
Helper_t<TemplateList<Tail...>,
typename std::conditional<Contain<SortedList, Head>::value,
SortedList,
Prepend_t<Helper_t<InheritFrom_t<Head>, SortedList>, Head>>::type>;
};
public:
using type = Helper_t<TemplateList<Base, Bases...>, TemplateList<>>;
};
template<template<typename...> class Base, template<typename...> class... Bases>
using TopoSort_t = typename TopoSort<Base, Bases...>::type;
现在可以对之前提及的多继承结构进行自动排序,并配合 LI
辅助类型展开排序结果;但因为参数传递问题,所以排序算法应该由 LI
的 type
部分调用,我们不应该主动调用 TopoSort
获取结果。
也就是说 LI
中 public
部分的 type
内,需要将 TemplateList
更改为 TopoSort_t
;因改动过小故不在这里重复放出代码。
我们可以利用 C++ 中基类构造顺序优先于派生类的性质查看继承顺序。
#include <iostream>
struct Nil {};
template<typename Base>
struct A : public Base {
A() { std::cout << "A "; }
};
template<typename Base>
struct B : public Base {
B() { std::cout << "B "; }
};
template<typename Base>
struct C : public Base {
C() { std::cout << "C "; }
};
InheritanceRegister( B, A );
InheritanceRegister( C, A );
struct G : public LI<B, C>::template type<Nil> {
G() { std::cout << "G "; }
};
int main() { G derived; }
这与我们手动拓扑排序的结果是相同的。
在线运行以上代码。
我们可以给出一个更复杂的继承关系图:
它的拓扑排序应该是:
同样能够拿到一个完全一致的排序结果:
在线运行以上代码。
3.区分虚拟继承
上述代码刻意忽略了非虚拟继承,但这两种继承关系是不同的;C++ 允许混用虚拟继承和非虚拟继承,这会导致继承关系图中出现重复类实例:
虽然不知道这种设计的目的是什么,但是重复出现的类型 A 显然不能被直接剔除。
前文实现的拓扑排序假定所有继承关系都是虚拟继承,因此无法处理这种存在非虚拟继承的无环图。
对于这类复杂的继承关系,我们需要有一个明确的解析顺序,确保得到的排序结果能够“正常”工作。
C3 线性化算法在解析继承关系时会做包括但不限于以下保证:
- 按照继承列表中声明的父类顺序从左向右遍历继承关系
- 方法解析顺序局部优先
第一条约束明确了遍历顺序,第二条则确保了查找优先级是就近的。
实际上 C3 线性化算法还不允许子类的继承声明顺序与父类不同,也就是说
class C(A, B)
和class D(B, A)
是不允许同时出现的。
这是因为 Python 中每个类型的继承关系都被全局共享;但 C++ 的类型系统足够静态,不同的最派生类都具有唯一的继承顺序,所以算法实现不需要考虑这种偏序关系。
遍历顺序与我们无关,但是方法解析顺序的局部优先很重要。局部优先是指在 Python 中查找一个类方法时,会按照:当前类 -> 当前类的直接父类 -> 当前类的祖先类的顺序查找;其中,在直接父类中的查找顺序又会按照继承列表中的父类顺序,从左到右查找,一旦找到立即停止并返回。
体现在类继承关系中则是:在注册继承关系时,靠左的基类会被优先放置在继承链中更靠近派生类的位置上。
这是因为在单继承场景下,如果两个基类具有同名的非虚方法,那么离最派生类更远的基类的方法会因为名称覆盖而被隐藏,此时只有更靠近最派生类的基类的方法能被无名称限定地访问。
struct A {
void foo();
};
struct B : public A {
void foo(); // 定义了同名的非虚函数,隐藏了 A::foo()
};
struct C : public B {};
C derived;
derived.foo(); // 此时调用的是 B::foo()
所以在处理含非虚拟继承的关系图时,我们要将出现在依赖列表左侧的基类放置在排序结果的末尾(假定排序结果开头是第一个基类),并且每个类与它的直接基类都应该靠得尽可能近。
对于前文提及的混合继承结构,我们可以按照要求手动求出它的一个拓扑序:
读者可能会疑惑:如果我需要在类 G 中使用名称限定访问两个不同的类 A 实例,我该怎么操作?
很可惜在这种线性化继承场景下,使用基类名称引用基类实例会变得极为困难。因为现在每一个基类都是模板类,因此基类的实际名称是存在大量嵌套关系的,如上图中左起第二个类 A 的名称是A<C<A<Nil>>>
,这很显然是极难人工写出的。
但是我们可以借助工具自动获取类名,读者可以获取拓扑排序后的结果列表,然后在列表中查找所需的类实例位置,并在该点截断列表,最后展开被截断的列表就能得到类名。
此时我们要同时处理两种继承关系,并且两种继承关系都可以出现在最派生类的声明当中,所以我们需要修改 TopoSort
和 LI
的参数列表。
因为现在涉及了非虚拟继承关系的处理,InheritFrom
和 TopoSort
都需要重新设计,故代码会膨胀不少。
因为现在 InheritFrom
需要记录两个类型列表,所以除了原有的辅助宏外还需要引入一个新的辅助宏,用于解决宏函数中逗号识别的问题。
template<template<typename...> class Node>
struct InheritFrom {
using VBs = TemplateList<>; // virtual base
using NVBs = TemplateList<>; // non-virtual base
};
template<template<typename...> class Node>
using InheritFrom_vbt = typename InheritFrom<Node>::VBs;
template<template<typename...> class Node>
using InheritFrom_nvbt = typename InheritFrom<Node>::NVBs;
// Require 的作用是将若干个逗号分开的符号打包成一个
#define Require( ... ) __VA_ARGS__
#define InheritanceRegister( Node, VBList, NVBList ) \
template<> \
struct InheritFrom<Node> { \
using VBs = TemplateList<VBList>; \
using NVBs = TemplateList<NVBList>; \
}
解析过程基本上遵循着几个步骤:非虚拟继承基类直接加入到排序结果中;虚拟继承基类则需要遍历已访问的虚基类列表,根据该列表是否包含自身与否决定是否将自己加入到排序结果中。其余操作均为实现细节。
与只处理虚拟继承关系的拓扑排序相同,此处有多种方式遍历关系图并得到一个结果,所以只给出一个我认为比较简洁的:
// NVIList: Non-virtual Base List, VBList: Virtual Base List
template<typename NVBList, typename VBList = TemplateList<>>
struct TopoSort; // 主模板
// NVB: Non-virtual Base, VB: Virtual Base
template<template<typename...> class NVB,
template<typename...> class... NVBs,
template<typename...> class... VBs>
struct TopoSort<TemplateList<NVB, NVBs...>, TemplateList<VBs...>> { // 特化版本
private:
// VI: Virtual Inherit
template<bool VI, typename List, typename SortedList, typename VisitedVBs>
struct Helper;
/* Return the virtual base class node that was accessed during the recursive process.
* pt: path type. */
template<bool VI, typename List, typename SortedList, typename VisitedVBs>
using Helper_pt = typename Helper<VI, List, SortedList, VisitedVBs>::path;
/* Return the sorted list.
* tp: type. */
template<bool VI, typename List, typename SortedList, typename VisitedVBs>
using Helper_tp = typename Helper<VI, List, SortedList, VisitedVBs>::type;
template<bool VI, typename SortedList, typename VisitedVBs>
struct Helper<VI, TemplateList<>, SortedList, VisitedVBs> {
using path = VisitedVBs;
using type = SortedList;
};
template<template<typename...> class Head,
template<typename...> class... Tail,
typename SortedList,
typename VisitedVBs>
struct Helper<true, TemplateList<Head, Tail...>, SortedList, VisitedVBs> {
private:
using SortVB_t = Helper_tp<true, InheritFrom_vbt<Head>, SortedList, VisitedVBs>;
using MarkVB_t = Helper_pt<true, InheritFrom_vbt<Head>, SortedList, VisitedVBs>;
using SortTail_t = Helper_tp<true, TemplateList<Tail...>, SortVB_t, MarkVB_t>;
using MarkTail_t = Helper_pt<true, TemplateList<Tail...>, SortVB_t, MarkVB_t>;
using SortNVB_t = Helper_tp<false, InheritFrom_nvbt<Head>, SortTail_t, MarkTail_t>;
using MarkNVB_t = Helper_pt<false, InheritFrom_nvbt<Head>, SortTail_t, MarkTail_t>;
public:
using path = Prepend_t<MarkNVB_t, Head>;
using type =
typename std::conditional<Contain<VisitedVBs, Head>::value || Contain<MarkTail_t, Head>::value,
Helper_tp<true, TemplateList<Tail...>, SortedList, VisitedVBs>,
Prepend_t<SortNVB_t, Head>>::type;
};
template<template<typename...> class Head,
template<typename...> class... Tail,
typename SortedList,
typename VisitedVBs>
struct Helper<false, TemplateList<Head, Tail...>, SortedList, VisitedVBs> {
private:
using SortVB_t = Helper_tp<true, InheritFrom_vbt<Head>, SortedList, VisitedVBs>;
using MarkVB_t = Helper_pt<true, InheritFrom_vbt<Head>, SortedList, VisitedVBs>;
using SortTail_t = Helper_tp<false, TemplateList<Tail...>, SortVB_t, MarkVB_t>;
using MarkTail_t = Helper_pt<false, TemplateList<Tail...>, SortVB_t, MarkVB_t>;
using SortNVB_t = Helper_tp<false, InheritFrom_nvbt<Head>, SortTail_t, MarkTail_t>;
using MarkNVB_t = Helper_pt<false, InheritFrom_nvbt<Head>, SortTail_t, MarkTail_t>;
public:
using path = MarkNVB_t;
using type = Prepend_t<SortNVB_t, Head>;
};
public:
using type = Helper_tp<false,
TemplateList<NVB, NVBs...>,
Helper_tp<true, TemplateList<VBs...>, TemplateList<>, TemplateList<>>,
Helper_pt<true, TemplateList<VBs...>, TemplateList<>, TemplateList<>>>;
};
template<typename NVBList, typename VBList = TemplateList<>>
using TopoSort_t = typename TopoSort<NVBList, VBList>::type;
TopoSort
引入的虚拟继承依赖声明并不总是必须的,尤其是当这些虚基类已经被基类声明依赖时,显式传入的虚基类列表就会被忽略;所以这里额外提供了一个更简洁的 LI_t
函数:
template<typename NVBList, typename VBList = TemplateList<>>
struct LI;
template<template<typename...> class NVB,
template<typename...> class... NVBs,
template<typename...> class... VBs>
struct LI<TemplateList<NVB, NVBs...>, TemplateList<VBs...>> {
private:
template<typename TopoOrder, typename RBC, typename... Args>
struct Helper;
template<typename TopoOrder, typename RBC, typename... Args>
using Helper_t = typename Helper<TopoOrder, RBC, Args...>::type;
template<typename RBC, typename... Args>
struct Helper<TemplateList<>, RBC, Args...> {
using type = RBC;
};
template<template<typename...> class Head,
template<typename...> class... Tail,
typename RBC,
typename... Args>
struct Helper<TemplateList<Head, Tail...>, RBC, Args...> {
using type = Head<Helper_t<TemplateList<Tail...>, RBC, Args...>, Args...>;
};
public:
// RBC: Root Base Class
template<typename RBC, typename... Args>
using type = Helper_t<TopoSort_t<TemplateList<NVB, NVBs...>, TemplateList<VBs...>>, RBC, Args...>;
};
template<template<typename...> class NVB, template<typename...> class... NVBs>
struct LI_t {
template<typename RBC, typename... Args>
using type = typename LI<TemplateList<NVB, NVBs...>>::type<RBC, Args...>;
};
用之前提到的混合继承结构验证一下算法正确性:
有一个无关紧要的基类顺序不同,其他整体上是对的。
在线运行以上代码。
对于更复杂的继承结构也同样适用:
这里就不手动排序了,感兴趣的读者可以在线运行代码后查看排序结果。
4.其他模板参数支持
正如引入 LI
的部分所提及的,本文实现的算法完全支持 CRTP 等带有额外模板参数的设计模式。
实际上,设计这个工具的初衷就是为了在虚拟继承环境下使用 CRTP 设计。
template<typename Base, typename Derived>
struct A : public Base { /* ... */ };
template<typename Base, typename Derived>
struct B : public Base { /* ... */ };
struct G : LI_t<A, B>::template type<Nil, G> { /* ... */ };
关于 CRTP 的使用,可以参照这个项目中部分类型的继承设计。
但是无论是 LI
还是 TopoSort
都假定传入的参数类型是纯粹的模板模板类型,因此我们无法直接支持含有非类型参数的模板类型。
template<size_t N>
struct Array; // No!
并且 LI
还要求展开后的拓扑排序结果中,所有模板类的模板参数数量都必须相同。故如果读者有额外的需求,可以自行设计一个 LI
类。
不过 TopoSort
可以引入包装类型解决参数列表不兼容的问题。
// 有一个带有非类型参数的模板类
template <int N>
struct Number { /* ... */ };
// 可以定义一个类型包装器
template <typename WrappedType>
struct Adapter;
template <int N>
struct Adapter<Number<N>> {
using type = Number<N>;
};
// 然后就可以使用 Adapter 代替 Number
5.Reference
本文的灵感来源、代码实现以及撰写思路完全受到这篇文章的启发。