如何為您的類別階層設定 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 最基本的工作設定需要以下步驟
在您宣告
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
。一個常見的命名慣例是將這些列舉稱為「種類」,以避免與「類型」或「類別」等詞語產生歧義,這些詞語在 LLVM 的許多上下文中具有重載的含義。有時會有一個自然的名稱,比如「操作碼」。不要在這個問題上過於糾結;如果有疑問,請使用
Kind
。你可能會好奇為什麼
Kind
列舉沒有Shape
的項目。這是因為Shape
是抽象的(computeArea() = 0;
),所以你永遠不會真正擁有該類別的確切非衍生實例(只有子類別)。請參閱具體基類和更深層次結構,了解如何處理非抽象基類。這裡值得一提的是,與dynamic_cast<>
不同,LLVM 風格的 RTTI 可以(並且經常)用於沒有虛擬函式表的類別。接下來,您需要確保將
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
都是 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 中的 Decl 與 DeclContext。 Decl
階層的完成方式與本教學中演示的範例設置非常相似。關鍵部分是如何結合 DeclContext
:所有需要做的都在 bool DeclContext::classof(const Decl *)
中,它詢問「給定一個 Decl
,我如何確定它是否是 DeclContext
的子類別?」。它通過對 Decl
「種類」集合的簡單切換來回答這個問題,並針對已知是 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
。