クラス階層に LLVM スタイルの RTTI を設定する方法¶
背景¶
LLVM は、C++ の組み込み RTTI の使用を避けています。代わりに、LLVM は、はるかに効率的で柔軟性のある独自の手作りの 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
を宣言するヘッダーで、LLVM の RTTI テンプレートを宣言する#include "llvm/Support/Casting.h"
を記述する必要があります。そうすれば、クライアントはそれを考える必要さえありません。#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
メンバーはカプセル化して private にしておき、列挙型ShapeKind
はgetKind()
メソッドを提供するとともに public にします。これは、クライアントが列挙型に対してswitch
を実行できるため便利です。一般的な命名規則では、これらの列挙型は「種類」であり、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; };
最後に、クラスの型を動的に決定する方法(つまり、
isa<>
/dyn_cast<>
が成功するかどうか)を LLVM の RTTI テンプレートに知らせる必要があります。デフォルトの「ユースケースの 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
の役割は、基底クラスのオブジェクトが tatsächlich 特定の派生クラスのオブジェクトであるかどうかを動的に判断することです。型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
の「is-a」であり、classof
はそれらに対して true
を返す必要があるためです。
このアプローチは、任意の深さの階層にスケールするようにすることができます。コツは、列挙値をクラス階層ツリーの先行順走査に対応するように配置することです。その配置により、上記のすべてのサブクラスのテストは、上記のように 2 つの比較で実行できます。クラス階層を箇条書きのようにリストするだけで、正しい順序が得られます。
| 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内部の Decl と DeclContext があります。Decl
階層は、このチュートリアルで示されているサンプル設定と非常によく似た方法で行われます。重要な部分は、DeclContext
をどのように組み込むかです。bool DeclContext::classof(const Decl *)
に必要なすべてが含まれています。これは、「Decl
が与えられた場合、それが DeclContext
であるかどうかをどのように判断できるか?」という質問です。これは、Decl
の「種類」の集合に対する単純なswitch文で、DeclContext
であることがわかっているものに対してtrueを返すことで、この質問に答えます。
経験則¶
Kind
列挙型は、具象クラスごとに1つのエントリを持ち、継承ツリーの先行順巡回に従って順序付けられる必要があります。classof
の引数は、const Base *
である必要があります。ここで、Base
は継承階層内の祖先です。引数は、派生クラスやクラス自体であってはなりません。isa<>
のテンプレート機構は、すでにこのケースを処理し、最適化しています。子を持たない階層内の各クラスに対して、その
Kind
のみを確認するclassof
を実装します。子を持つ階層内の各クラスに対して、最初の子の
Kind
と最後の子のKind
の範囲を確認するclassof
を実装します。
オープンなクラス階層のRTTI¶
階層内のすべての型を事前に把握できない場合があります。たとえば、上記の形状階層では、作成者はコードがユーザー定義の形状でも機能するようにしたいと考えていた可能性があります。オープンな階層を必要とするユースケースをサポートするために、LLVMは RTTIRoot
と RTTIExtends
ユーティリティを提供します。
RTTIRoot
クラスは、RTTIチェックを実行するためのインターフェースを記述します。RTTIExtends
クラステンプレートは、RTTIRoot
から派生したクラスに対してこのインターフェースの実装を提供します。RTTIExtends
は、「奇妙な再帰テンプレートパターン」を使用し、定義されているクラスを最初のテンプレート引数として、親クラスを2番目の引数として取ります。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
は、isPossible
、doCast
、castFailed
、doCastIfPossible
の4つのメソッドを提供します。これらは、それぞれ isa
、cast
、dyn_cast
に対応します。キャストの実行方法を制御するには、基本 CastInfo
構造体と同じ静的メソッドを提供する CastInfo
構造体(目的の型への)の特殊化を作成します。
これは多くの定型文になる可能性があるため、キャストトレイトと呼ばれるものもあります。これらは、上記のメソッドの1つ以上を提供する構造体であるため、プロジェクト内の共通のキャストパターンをファクタリングできます。ヘッダーファイルには、すぐに使用できるものがいくつか用意されており、その使用方法を説明する例をいくつか示します。これらの例は網羅的ではなく、新しいキャストトレイトの追加は簡単なので、ユーザーは自由にプロジェクトに追加したり、特に役立つ場合は貢献したりできます。
値から値へのキャスト¶
この場合、「null許容」と呼ばれる構造体があります。つまり、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
になります。