如何为您的类层次结构设置 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
需要注意的一个错误¶
刚刚给出的示例打开了错误的大门,在这些错误中,当向层次结构中添加(或删除)类时,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;
}
如注释所示,此代码包含错误。避免此错误的一种简单且不巧妙的方法是在添加第一个子类时在枚举中引入显式的 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
的契约是“如果参数的动态类型是 C
的子类型,则返回 true
”。只要您的实现满足此契约,就可以根据需要调整和优化它。
例如,LLVM 风格的 RTTI 可以通过定义适当的 classof
在存在多重继承的情况下正常工作。Clang 中 Decl 与 DeclContext 就是一个实际例子。 Decl
的继承体系结构与本教程中演示的示例设置非常相似。关键在于如何整合 DeclContext
:只需要在 bool DeclContext::classof(const Decl *)
中进行操作,它会询问“给定一个 Decl
,如何确定它是否是 DeclContext
的子类型?”。它通过对 Decl
的“种类”集合进行简单的 switch 语句,并对已知是 DeclContext
的种类返回 true 来回答这个问题。
经验法则¶
Kind
枚举应为每个具体类提供一个条目,并按照继承树的前序遍历顺序排列。classof
的参数应为const Base *
,其中Base
是继承层次结构中的某个祖先类。参数**绝不**应该是派生类或类本身:isa<>
的模板机制已经处理了这种情况并进行了优化。对于继承层次结构中没有子类的每个类,实现一个仅针对其
Kind
进行检查的classof
。对于继承层次结构中具有子类的每个类,实现一个检查第一个子类的
Kind
和最后一个子类的Kind
范围的classof
。
开放类层次结构的 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
结构体相同的静态方法。
这可能会产生大量样板代码,因此我们还提供了一些我们称之为转换特征的东西。这些是提供上述一个或多个方法的结构体,因此您可以在项目中提取常见的转换模式。我们在头文件中提供了一些现成的转换特征,并将展示一些激励其用法的示例。这些示例并非详尽无遗,添加新的转换特征非常容易,因此用户可以随意将其添加到他们的项目中,或者在它们特别有用时贡献它们!
值到值转换¶
在这种情况下,我们有一个我们称为“可空”的结构体,即它可以从 nullptr
构造,并且会导致一个可以识别为无效的值。
class SomeValue {
public:
SomeValue(void *ptr) : ptr(ptr) {}
void *getPointer() const { return ptr; }
bool isValid() const { return ptr != nullptr; }
private:
void *ptr;
};
鉴于类似这样的情况,我们希望按值传递此对象,并且希望能够从此类型的对象转换为另一组对象。目前,我们假设我们想要转换到的类型都提供了 classof
。因此,我们可以使用一些提供的转换特征,如下所示
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
,也许我们希望能够从字符指针类型转换为该类型。在这种情况下,我们将执行以下操作
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> {};
该转换特征要求 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
。