如何为你的类层次结构设置 LLVM 风格的 RTTI¶
背景¶
LLVM 避免使用 C++ 内置的 RTTI。相反,它广泛使用自己手写的 RTTI 形式,这种形式更高效、更灵活,尽管它需要类作者做更多的工作。
从客户端的角度来看,如何使用 LLVM 风格的 RTTI 的描述在 程序员手册 中给出。本文档与之相反,讨论了作为类层次结构作者,你需要采取哪些步骤来使 LLVM 风格的 RTTI 可供你的客户端使用。
在深入研究之前,请确保你熟悉面向对象编程中的 “is-a” 概念。
基本设置¶
本节介绍如何设置最基本的 LLVM 风格 RTTI 形式(这足以满足 99.9% 的情况)。我们将为以下类层次结构设置 LLVM 风格的 RTTI
class Shape {
public:
Shape() {}
virtual double computeArea() = 0;
};
class Square : public Shape {
double SideLength;
public:
Square(double S) : SideLength(S) {}
double computeArea() override;
};
class Circle : public Shape {
double Radius;
public:
Circle(double R) : Radius(R) {}
double computeArea() override;
};
最基本可用的 LLVM 风格 RTTI 设置需要以下步骤
在你声明
Shape
的头文件中,你将需要#include "llvm/Support/Casting.h"
,它声明了 LLVM 的 RTTI 模板。这样,你的客户端甚至不必考虑它。#include "llvm/Support/Casting.h"
在基类中,引入一个枚举,区分层次结构中所有不同的具体类,并将枚举值存储在基类中的某个位置。
以下是引入此更改后的代码
class Shape { public: + /// Discriminator for LLVM-style RTTI (dyn_cast<> et al.) + enum ShapeKind { + SK_Square, + SK_Circle + }; +private: + const ShapeKind Kind; +public: + ShapeKind getKind() const { return Kind; } + Shape() {} virtual double computeArea() = 0; };
你通常会希望保持
Kind
成员的封装性和私有性,但让枚举ShapeKind
与提供getKind()
方法一起公开。这对于客户端来说很方便,以便他们可以对枚举进行switch
。一个常见的命名约定是这些枚举是 “kind”,以避免与 “type” 或 “class” 这些词的歧义,因为这些词在 LLVM 的许多上下文中具有重载的含义。有时会有一个自然的名称,例如 “opcode”。不要在这些细节上争论不休;当有疑问时,使用
Kind
。你可能想知道为什么
Kind
枚举没有Shape
的条目。原因是由于Shape
是抽象的 (computeArea() = 0;
),你永远不会真正拥有该类的非派生实例(只有子类)。有关如何处理非抽象基类的信息,请参阅 具体基类和更深层次的继承结构。值得在这里提到的是,与dynamic_cast<>
不同,LLVM 风格的 RTTI 可以用于(并且经常用于)没有 v-table 的类。接下来,你需要确保
Kind
被初始化为与类的动态类型对应的值。通常,你希望将其作为基类构造函数的参数,然后从子类构造函数中传入相应的XXXKind
。以下是该更改后的代码
class Shape { public: /// Discriminator for LLVM-style RTTI (dyn_cast<> et al.) enum ShapeKind { SK_Square, SK_Circle }; private: const ShapeKind Kind; public: ShapeKind getKind() const { return Kind; } - Shape() {} + Shape(ShapeKind K) : Kind(K) {} virtual double computeArea() = 0; }; class Square : public Shape { double SideLength; public: - Square(double S) : SideLength(S) {} + Square(double S) : Shape(SK_Square), SideLength(S) {} double computeArea() override; }; class Circle : public Shape { double Radius; public: - Circle(double R) : Radius(R) {} + Circle(double R) : Shape(SK_Circle), Radius(R) {} double computeArea() override; };
最后,你需要告知 LLVM 的 RTTI 模板如何动态确定类的类型(即
isa<>
/dyn_cast<>
是否应该成功)。完成此操作的默认 “99.9% 的用例” 方式是通过一个小的静态成员函数classof
。为了对解释有适当的上下文,我们将首先显示此代码,然后在下面描述每个部分class Shape { public: /// Discriminator for LLVM-style RTTI (dyn_cast<> et al.) enum ShapeKind { SK_Square, SK_Circle }; private: const ShapeKind Kind; public: ShapeKind getKind() const { return Kind; } Shape(ShapeKind K) : Kind(K) {} virtual double computeArea() = 0; }; class Square : public Shape { double SideLength; public: Square(double S) : Shape(SK_Square), SideLength(S) {} double computeArea() override; + + static bool classof(const Shape *S) { + return S->getKind() == SK_Square; + } }; class Circle : public Shape { double Radius; public: Circle(double R) : Shape(SK_Circle), Radius(R) {} double computeArea() override; + + static bool classof(const Shape *S) { + return S->getKind() == SK_Circle; + } };
classof
的工作是动态确定基类的对象是否实际上是特定的派生类。为了将类型Base
向下转型为类型Derived
,需要在Derived
中有一个classof
,它将接受类型为Base
的对象。具体来说,考虑以下代码
Shape *S = ...; if (isa<Circle>(S)) { /* do something ... */ }
此代码中
isa<>
测试的代码最终会归结为——在模板实例化和其他一些机制之后——大致类似于Circle::classof(S)
的检查。有关更多信息,请参阅 classof 的约定。classof
的参数应该始终是祖先类,因为该实现具有允许和自动优化向上转型/向上isa<>
的逻辑。就好像每个类Foo
自动具有一个classof
,例如class Foo { [...] template <class T> static bool classof(const T *, ::std::enable_if< ::std::is_base_of<Foo, T>::value >::type* = 0) { return true; } [...] };
请注意,这就是我们不需要在
Shape
中引入classof
的原因:所有相关的类都派生自Shape
,并且Shape
本身是抽象的(在Kind
枚举中没有条目),因此这个概念上推断出的classof
就是我们所需要的全部。有关如何将此示例扩展到更一般的层次结构的更多信息,请参阅 具体基类和更深层次的继承结构。
虽然对于这个小例子来说,设置 LLVM 风格的 RTTI 看起来像是很多 “样板代码”,但如果你的类正在做任何有趣的事情,那么这将最终只占代码的一小部分。
具体基类和更深层次的继承结构¶
对于具体基类(即继承树的非抽象内部节点),classof
内部的 Kind
检查需要稍微复杂一些。这种情况与上面的示例不同,因为
由于该类是具体的,因此它本身必须在
Kind
枚举中有一个条目,因为可能存在以该类作为动态类型的对象。由于该类有子类,因此
classof
内部的检查必须将它们考虑在内。
假设 SpecialSquare
和 OtherSpecialSquare
派生自 Square
,因此 ShapeKind
变为
enum ShapeKind {
SK_Square,
+ SK_SpecialSquare,
+ SK_OtherSpecialSquare,
SK_Circle
}
然后在 Square
中,我们需要像这样修改 classof
- static bool classof(const Shape *S) {
- return S->getKind() == SK_Square;
- }
+ static bool classof(const Shape *S) {
+ return S->getKind() >= SK_Square &&
+ S->getKind() <= SK_OtherSpecialSquare;
+ }
我们需要像这样测试一个范围而不是仅仅相等的原因是,SpecialSquare
和 OtherSpecialSquare
都是 “is-a” Square
,因此 classof
需要为它们返回 true
。
这种方法可以扩展到任意深度的层次结构。诀窍在于你安排枚举值,使其对应于类层次结构树的先序遍历。通过这种安排,所有子类测试都可以像上面所示那样通过两次比较来完成。如果你只是像项目符号列表一样列出类层次结构,你将获得正确的顺序
| Shape
| Square
| SpecialSquare
| OtherSpecialSquare
| Circle
需要注意的 Bug¶
刚刚给出的示例打开了 bug 的大门,当向层次结构添加(或从中删除)类时,classof
没有更新以匹配 Kind
枚举。
继续上面的示例,假设我们添加一个 SomewhatSpecialSquare
作为 Square
的子类,并像这样更新 ShapeKind
枚举
enum ShapeKind {
SK_Square,
SK_SpecialSquare,
SK_OtherSpecialSquare,
+ SK_SomewhatSpecialSquare,
SK_Circle
}
现在,假设我们忘记更新 Square::classof()
,所以它仍然看起来像
static bool classof(const Shape *S) {
// BUG: Returns false when S->getKind() == SK_SomewhatSpecialSquare,
// even though SomewhatSpecialSquare "is a" Square.
return S->getKind() >= SK_Square &&
S->getKind() <= SK_OtherSpecialSquare;
}
正如注释所示,此代码包含一个 bug。避免这种情况的直接且不复杂的方法是在添加第一个子类时,在枚举中引入一个显式的 SK_LastSquare
条目。例如,我们可以将 具体基类和更深层次的继承结构 开头的示例重写为
enum ShapeKind {
SK_Square,
+ SK_SpecialSquare,
+ SK_OtherSpecialSquare,
+ SK_LastSquare,
SK_Circle
}
...
// Square::classof()
- static bool classof(const Shape *S) {
- return S->getKind() == SK_Square;
- }
+ static bool classof(const Shape *S) {
+ return S->getKind() >= SK_Square &&
+ S->getKind() <= SK_LastSquare;
+ }
然后,添加新的子类很容易
enum ShapeKind {
SK_Square,
SK_SpecialSquare,
SK_OtherSpecialSquare,
+ SK_SomewhatSpecialSquare,
SK_LastSquare,
SK_Circle
}
请注意,Square::classof
不需要更改。
classof
的约定¶
更准确地说,假设 classof
在类 C
内部。那么 classof
的约定是 “如果参数的动态类型是 is-a C
,则返回 true
”。只要你的实现满足此约定,你就可以根据需要调整和优化它。
例如,通过定义适当的 classof
,LLVM 风格的 RTTI 可以在多重继承的情况下正常工作。Clang 中的 Decl 与 DeclContext 就是一个实际的例子。Decl
层次结构的完成方式与本教程中演示的示例设置非常相似。关键部分是如何将 DeclContext
合并进来:所有需要的都在 bool DeclContext::classof(const Decl *)
中,它提出了问题 “给定一个 Decl
,我如何确定它是否是 is-a DeclContext
?”。它通过对 Decl
“种类” 集合进行简单的 switch,并为已知是 DeclContext
的种类返回 true 来回答这个问题。
经验法则¶
Kind
枚举应为每个具体类设置一个条目,并根据继承树的先序遍历进行排序。classof
的参数应该是const Base *
,其中Base
是继承层次结构中的某个祖先。参数绝不应该是派生类或类本身:isa<>
的模板机制已经处理了这种情况并对其进行了优化。对于层次结构中没有子类的每个类,实现一个
classof
,该classof
仅针对其Kind
进行检查。对于层次结构中具有子类的每个类,实现一个
classof
,该classof
检查第一个子类的Kind
和最后一个子类的Kind
的范围。
开放类层次结构的 RTTI¶
有时不可能预先知道层次结构中的所有类型。例如,在上面描述的形状层次结构中,作者可能希望他们的代码也适用于用户定义的形状。为了支持需要开放层次结构的用例,LLVM 提供了 RTTIRoot
和 RTTIExtends
实用程序。
RTTIRoot
类描述了执行 RTTI 检查的接口。RTTIExtends
类模板为从 RTTIRoot
派生的类提供了此接口的实现。RTTIExtends
使用 “奇特的递归模板模式”,将正在定义的类作为其第一个模板参数,将父类作为第二个参数。任何使用 RTTIExtends
的类都必须定义一个 static char ID
成员,其地址将用于标识类型。
只有在你的用例需要时才应使用此开放层次结构 RTTI 支持。否则,应首选标准的 LLVM RTTI 系统。
例如:
class Shape : public RTTIExtends<Shape, RTTIRoot> {
public:
static char ID;
virtual double computeArea() = 0;
};
class Square : public RTTIExtends<Square, Shape> {
double SideLength;
public:
static char ID;
Square(double S) : SideLength(S) {}
double computeArea() override;
};
class Circle : public RTTIExtends<Circle, Shape> {
double Radius;
public:
static char ID;
Circle(double R) : Radius(R) {}
double computeArea() override;
};
char Shape::ID = 0;
char Square::ID = 0;
char Circle::ID = 0;
高级用例¶
isa/cast/dyn_cast 的底层实现都通过一个名为 CastInfo
的结构体来控制。CastInfo
提供了 4 个方法:isPossible
、doCast
、castFailed
和 doCastIfPossible
。这些方法依次用于 isa
、cast
和 dyn_cast
。你可以通过创建 CastInfo
结构体的特化(针对你想要的类型),并提供与基本 CastInfo
结构体相同的静态方法,来控制你的转换的执行方式。
这可能有很多样板代码,因此我们也有所谓的 Cast Traits。这些是提供上述一个或多个方法的结构体,因此你可以在你的项目中分解出常见的转换模式。我们在头文件中提供了一些可以直接使用的 Cast Traits,我们将展示一些例子来激发它们的使用。这些例子并非详尽无遗,添加新的 cast traits 很容易,因此用户应该随意将它们添加到他们的项目中,或者在它们特别有用时贡献它们!
值到值的转换¶
在这种情况下,我们有一个我们称之为 “可为空” 的结构体 - 即,它可以从 nullptr
构造,并且结果是一个你可以判断为无效的值。
class SomeValue {
public:
SomeValue(void *ptr) : ptr(ptr) {}
void *getPointer() const { return ptr; }
bool isValid() const { return ptr != nullptr; }
private:
void *ptr;
};
给定类似这样的东西,我们想通过值传递这个对象,并且我们想从这种类型的对象转换为其他一些对象集。现在,我们假设我们想要转换到的所有类型都提供 classof
。因此,我们可以像这样使用一些提供的 cast traits
template <typename T>
struct CastInfo<T, SomeValue>
: CastIsPossible<T, SomeValue>, NullableValueCastFailed<T>,
DefaultDoCastIfPossible<T, SomeValue, CastInfo<T, SomeValue>> {
static T doCast(SomeValue v) {
return T(v.getPointer());
}
};
指针到值的转换¶
现在给定上面的值 SomeValue
,也许我们想能够从 char 指针类型转换为该类型。所以在这种情况下我们会这样做
template <typename T>
struct CastInfo<SomeValue, T *>
: NullableValueCastFailed<SomeValue>,
DefaultDoCastIfPossible<SomeValue, T *, CastInfo<SomeValue, T *>> {
static bool isPossible(const T *t) {
return std::is_same<T, char>::value;
}
static SomeValue doCast(const T *t) {
return SomeValue((void *)t);
}
};
如果我们想这样做,这将使我们能够从 char *
转换为 SomeValue。
可选值转换¶
当你的类型不能从 nullptr
构造,或者没有简单的方法来判断对象何时无效时,你可能想要使用 std::optional
。在这些情况下,你可能想要类似这样的东西
template <typename T>
struct CastInfo<T, SomeValue> : OptionalValueCast<T, SomeValue> {};
该 cast trait 要求 T
可以从 const SomeValue &
构造,但它启用了如下转换
SomeValue someVal = ...;
std::optional<AnotherValue> valOr = dyn_cast<AnotherValue>(someVal);
使用 _if_present
变体,你甚至可以像这样进行可选链式调用
std::optional<SomeValue> someVal = ...;
std::optional<AnotherValue> valOr = dyn_cast_if_present<AnotherValue>(someVal);
如果 someVal
无法转换或者如果 someVal
也为 std::nullopt
,则 valOr
将为 std::nullopt
。