如何为你的类层次结构设置 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 设置需要以下步骤

  1. 在你声明 Shape 的头文件中,你将需要 #include "llvm/Support/Casting.h",它声明了 LLVM 的 RTTI 模板。这样,你的客户端甚至不必考虑它。

    #include "llvm/Support/Casting.h"
    
  2. 在基类中,引入一个枚举,区分层次结构中所有不同的具体类,并将枚举值存储在基类中的某个位置。

    以下是引入此更改后的代码

     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 的类。

  3. 接下来,你需要确保 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;
     };
    
  4. 最后,你需要告知 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 内部的检查必须将它们考虑在内。

假设 SpecialSquareOtherSpecialSquare 派生自 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;
+  }

我们需要像这样测试一个范围而不是仅仅相等的原因是,SpecialSquareOtherSpecialSquare 都是 “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 中的 DeclDeclContext 就是一个实际的例子。Decl 层次结构的完成方式与本教程中演示的示例设置非常相似。关键部分是如何将 DeclContext 合并进来:所有需要的都在 bool DeclContext::classof(const Decl *) 中,它提出了问题 “给定一个 Decl,我如何确定它是否是 is-a DeclContext?”。它通过对 Decl “种类” 集合进行简单的 switch,并为已知是 DeclContext 的种类返回 true 来回答这个问题。

经验法则

  1. Kind 枚举应为每个具体类设置一个条目,并根据继承树的先序遍历进行排序。

  2. classof 的参数应该是 const Base *,其中 Base 是继承层次结构中的某个祖先。参数绝不应该是派生类或类本身:isa<> 的模板机制已经处理了这种情况并对其进行了优化。

  3. 对于层次结构中没有子类的每个类,实现一个 classof,该 classof 仅针对其 Kind 进行检查。

  4. 对于层次结构中具有子类的每个类,实现一个 classof,该 classof 检查第一个子类的 Kind 和最后一个子类的 Kind 的范围。

开放类层次结构的 RTTI

有时不可能预先知道层次结构中的所有类型。例如,在上面描述的形状层次结构中,作者可能希望他们的代码也适用于用户定义的形状。为了支持需要开放层次结构的用例,LLVM 提供了 RTTIRootRTTIExtends 实用程序。

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 个方法:isPossibledoCastcastFaileddoCastIfPossible。这些方法依次用于 isacastdyn_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