如何為您的類別階層設定 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
的一種 (is-a) 型別,則返回 true
」。只要您的實作滿足此契約,您就可以根據需要盡可能多地調整和最佳化它。
例如,透過定義適當的 classof
,LLVM 風格 RTTI 可以在多重繼承的情況下正常運作。在實務中,Decl 與 Clang 內部的 DeclContext 就是一個範例。Decl
階層的完成方式與本教學中示範的範例設定非常相似。關鍵部分是如何整合 DeclContext
:所有需要的都在 bool DeclContext::classof(const Decl *)
中,它會詢問「給定一個 Decl
,我如何判斷它是否是 DeclContext
的一種 (is-a) 型別?」。它透過對 Decl
「種類」集合進行簡單的 switch 語句來回答這個問題,並對於已知是 DeclContext
的種類返回 true。
經驗法則¶
Kind
枚舉應該為每個具體類別都有一個條目,並根據繼承樹狀結構的前序遍歷排序。classof
的參數應該是const Base *
,其中Base
是繼承階層中的某個祖先。參數絕不應該是衍生類別或類別本身:isa<>
的樣板機制已經處理了這種情況並對其進行了最佳化。對於階層中沒有子類別的每個類別,實作一個僅針對其
Kind
進行檢查的classof
。對於階層中具有子類別的每個類別,實作一個檢查第一個子類別的
Kind
和最後一個子類別的Kind
範圍的classof
。
開放類別階層的 RTTI¶
有時不可能預先知道階層中的所有型別。例如,在上面描述的形狀階層中,作者可能希望他們的代码也能適用於使用者定義的形狀。為了支援需要開放階層的使用案例,LLVM 提供了 RTTIRoot
和 RTTIExtends
工具。
RTTIRoot
類別描述了執行 RTTI 檢查的介面。RTTIExtends
類別樣板為衍生自 RTTIRoot
的類別提供了此介面的實作。RTTIExtends
使用「奇特的遞迴範本模式 (Curiously Recurring Template Idiom)」,將正在定義的類別作為其第一個樣板參數,將父類別作為第二個參數。任何使用 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
結構相同的靜態方法。
這可能有很多樣板程式碼,因此我們也有所謂的 Cast Traits (轉型特徵)。這些是提供上述一個或多個方法的結構,因此您可以分解專案中的常見轉型模式。我們在標頭檔中提供了一些可供使用的轉型特徵,我們將展示一些範例來說明其用途。這些範例並不詳盡,新增新的轉型特徵很容易,因此使用者應隨時將它們新增到他們的專案中,或者如果它們特別有用,則貢獻它們!
值到值的轉型¶
在這種情況下,我們有一個稱為「可為空的 (nullable)」結構——即,它可以從 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
,也許我們希望能夠從 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> {};
該轉型特徵要求 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
。