クラス階層に 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 の最も基本的な設定には、次の手順が必要です。

  1. Shape を宣言するヘッダーで、LLVM の RTTI テンプレートを宣言する #include "llvm/Support/Casting.h" を記述する必要があります。そうすれば、クライアントはそれを考える必要さえありません。

    #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 メンバーはカプセル化して private にしておき、列挙型 ShapeKindgetKind() メソッドを提供するとともに public にします。これは、クライアントが列挙型に対して switch を実行できるため便利です。

    一般的な命名規則では、これらの列挙型は「種類」であり、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. 最後に、クラスの型を動的に決定する方法(つまり、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 にダウンキャストするには、Derivedclassof が存在し、型 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; }
      [...]
    };
    

    これが、Shapeclassof を導入する必要がなかった理由であることに注意してください。すべての関連クラスは Shape から派生し、Shape 自体は抽象的である(Kind 列挙型にエントリがない)ため、この概念的に推測される classof で十分です。この例をより一般的な階層に拡張する方法については、具象基底クラスとより深い階層 を参照してください。

この小さな例では、LLVM スタイルの RTTI の設定は多くの「ボイラープレート」のように見えますが、クラスが何か興味深いことを行っている場合、これはコードのごく一部になります.

具象基底クラスとより深い階層

具象基底クラス(つまり、継承ツリーの非抽象内部ノード)の場合、classof 内の Kind チェックはもう少し複雑にする必要があります。上記の例とは状況が異なり、

  • クラスは具象であるため、このクラスを動的型として持つオブジェクトを持つことができるため、Kind 列挙型にエントリが必要です。

  • クラスに子があるため、classof 内のチェックはそれらを考慮に入れる必要があります。

SpecialSquareOtherSpecialSquareSquare から派生し、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 の「is-a」であり、classof はそれらに対して true を返す必要があるためです。

このアプローチは、任意の深さの階層にスケールするようにすることができます。コツは、列挙値をクラス階層ツリーの先行順走査に対応するように配置することです。その配置により、上記のすべてのサブクラスのテストは、上記のように 2 つの比較で実行できます。クラス階層を箇条書きのようにリストするだけで、正しい順序が得られます。

| Shape
  | Square
    | SpecialSquare
    | OtherSpecialSquare
  | Circle

注意すべきバグ

上記の例では、階層にクラスを追加(または削除)するときに、classofKind 列挙型と一致するように更新されないバグが発生する可能性があります。

上記の例を続けると、SomewhatSpecialSquareSquare のサブクラスとして追加し、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 列挙型は、具象クラスごとに1つのエントリを持ち、継承ツリーの先行順巡回に従って順序付けられる必要があります。

  2. classof の引数は、const Base * である必要があります。ここで、Base は継承階層内の祖先です。引数は、派生クラスやクラス自体であってはなりません。isa<> のテンプレート機構は、すでにこのケースを処理し、最適化しています。

  3. 子を持たない階層内の各クラスに対して、その Kind のみを確認する classof を実装します。

  4. 子を持つ階層内の各クラスに対して、最初の子の Kind と最後の子の Kind の範囲を確認する classof を実装します。

オープンなクラス階層のRTTI

階層内のすべての型を事前に把握できない場合があります。たとえば、上記の形状階層では、作成者はコードがユーザー定義の形状でも機能するようにしたいと考えていた可能性があります。オープンな階層を必要とするユースケースをサポートするために、LLVMは RTTIRootRTTIExtends ユーティリティを提供します。

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 は、isPossibledoCastcastFaileddoCastIfPossible の4つのメソッドを提供します。これらは、それぞれ isacastdyn_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> {};

このキャストトレイトでは、Tconst 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 を変換できない場合、または someValstd::nullopt であった場合、valOrstd::nullopt になります。