如何为您的类层次结构设置 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

需要注意的一个错误

刚刚给出的示例打开了错误的大门,在这些错误中,当向层次结构中添加(或删除)类时,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 中 DeclDeclContext 就是一个实际例子。 Decl 的继承体系结构与本教程中演示的示例设置非常相似。关键在于如何整合 DeclContext:只需要在 bool DeclContext::classof(const Decl *) 中进行操作,它会询问“给定一个 Decl,如何确定它是否是 DeclContext 的子类型?”。它通过对 Decl 的“种类”集合进行简单的 switch 语句,并对已知是 DeclContext 的种类返回 true 来回答这个问题。

经验法则

  1. Kind 枚举应为每个具体类提供一个条目,并按照继承树的前序遍历顺序排列。

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

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

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

开放类层次结构的 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 结构体相同的静态方法。

这可能会产生大量样板代码,因此我们还提供了一些我们称之为转换特征的东西。这些是提供上述一个或多个方法的结构体,因此您可以在项目中提取常见的转换模式。我们在头文件中提供了一些现成的转换特征,并将展示一些激励其用法的示例。这些示例并非详尽无遗,添加新的转换特征非常容易,因此用户可以随意将其添加到他们的项目中,或者在它们特别有用时贡献它们!

值到值转换

在这种情况下,我们有一个我们称为“可空”的结构体,即它可以从 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