如何為您的類別階層設定 LLVM 風格的 RTTI

背景

LLVM 避免使用 C++ 內建的 RTTI。相反,它普遍使用自己手動建立的 RTTI 形式,這種形式效率更高、更靈活,儘管它需要您作為類別作者做更多工作。

關於如何從客戶端的角度使用 LLVM 風格 RTTI 的說明,請參閱程式設計師手冊。相比之下,本文檔討論了您作為類別階層作者需要採取的步驟,以便讓您的客戶端可以使用 LLVM 風格的 RTTI。

在深入探討之前,請確保您熟悉面向對象程式設計概念中的「是一種」。

基本設定

本節描述如何設定最基本的 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

    一個常見的命名慣例是將這些列舉稱為「種類」,以避免與「類型」或「類別」等詞語產生歧義,這些詞語在 LLVM 的許多上下文中具有重載的含義。有時會有一個自然的名稱,比如「操作碼」。不要在這個問題上過於糾結;如果有疑問,請使用 Kind

    你可能會好奇為什麼 Kind 列舉沒有 Shape 的項目。這是因為 Shape 是抽象的(computeArea() = 0;),所以你永遠不會真正擁有該類別的確切非衍生實例(只有子類別)。請參閱具體基類和更深層次結構,了解如何處理非抽象基類。這裡值得一提的是,與 dynamic_cast<> 不同,LLVM 風格的 RTTI 可以(並且經常)用於沒有虛擬函式表的類別。

  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 向下轉型為類型 DerivedDerived 中需要有一個 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 都是 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」。只要您的實作符合此合約,您就可以隨意調整和優化它。

例如,通過定義適當的 classof,LLVM 風格的 RTTI 可以在存在多重繼承的情況下正常工作。實際中的例子是 Clang 中的 DeclDeclContextDecl 階層的完成方式與本教學中演示的範例設置非常相似。關鍵部分是如何結合 DeclContext:所有需要做的都在 bool DeclContext::classof(const Decl *) 中,它詢問「給定一個 Decl,我如何確定它是否是 DeclContext 的子類別?」。它通過對 Decl「種類」集合的簡單切換來回答這個問題,並針對已知是 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