web-dev-qa-db-ja.com

RTTIを使用するよりも「純粋な多型」が望ましいのはなぜですか?

私がこれまで見てきたほとんどすべてのC++リソースは、RTTI(ランタイム型識別)を使用するよりも多態的なアプローチを好むべきだと言っています。一般に、私はこの種のアドバイスを真剣に受け止め、その理論的根拠を試して理解します-結局、C++は強力な獣であり、完全に理解するのは難しいです。ただし、この特定の質問については、空白を描いているので、インターネットがどのようなアドバイスを提供できるかを確認したいと思います。まず、RTTIが「有害と見なされる」理由として引用されている一般的な理由をリストすることで、これまでに学んだことを要約します。

一部のコンパイラはそれを使用しません/ RTTIは常に有効ではありません

私は本当にこの議論を買いません。それは、C++ 14の機能を使用すべきではないと言っているようなものです。C++ 14の機能をサポートしていないコンパイラがあるからです。それでも、C++ 14の機能を使うことを思いとどまらせる人はいません。大部分のプロジェクトは、使用しているコンパイラとその構成に影響を与えます。 gccのマンページを引用することもできます:

-fno-rtti

C++ランタイム型識別機能(dynamic_castおよびtypeid)が使用する仮想関数を使用して、すべてのクラスに関する情報の生成を無効にします。言語のこれらの部分を使用しない場合は、このフラグを使用してスペースを節約できます。例外処理では同じ情報が使用されますが、G ++は必要に応じて生成します。 dynamic_cast演算子は、実行時の型情報を必要としないキャスト、つまり「void *」または明確な基本クラスへのキャストに引き続き使用できます。

これが教えてくれるのは、ifRTTIを使用していないので、無効にすることができるということです。つまり、Boostを使用していない場合は、Boostにリンクする必要はありません。誰かが-fno-rttiでコンパイルしている場合に備えて計画する必要はありません。さらに、この場合、コンパイラーははっきりと失敗します。

メモリが余分にかかります/遅くなる可能性があります

RTTIを使用したいときはいつでも、何らかの種類の型情報またはクラスの特性にアクセスする必要があります。 RTTIを使用しないソリューションを実装する場合、これは通常、この情報を格納するためにクラスにいくつかのフィールドを追加する必要があることを意味します。そのため、メモリ引数は一種のvoidです(これについては後で説明します)。

Dynamic_castは確かに遅くなる可能性があります。ただし、通常は、速度が重要な状況で使用する必要がないようにする方法があります。そして、私はその代替案をまったく知りません。 This SO answer は、基本クラスで定義された列挙型を使用して型を格納することを提案します。これは、すべての派生クラスを事前に知っている場合にのみ機能します。かなり大きな「if」!

その答えから、RTTIのコストも明らかではないようです。異なる人々は異なるものを測定します。

エレガントなポリモーフィック設計により、RTTIは不要になります

これは私が真剣に受け止めている一種のアドバイスです。この場合、RTTIのユースケースをカバーする優れた非RTTIソリューションを思い付くことができません。例を挙げましょう。

ある種のオブジェクトのグラフを処理するライブラリを書いているとしましょう。ライブラリを使用するときにユーザーが独自の型を生成できるようにしたい(したがって、enumメソッドは使用できません)。ノードの基本クラスがあります。

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();
};

これで、ノードのタイプを変えることができます。これらはどうですか:

class red_node : virtual public node_base
{
  public:
    red_node();
    virtual ~red_node();

    void get_redness();
};

class yellow_node : virtual public node_base
{
  public:
    yellow_node();
    virtual ~yellow_node();

    void set_yellowness(int);
};

地獄、なぜこれらの1つでもない:

class orange_node : public red_node, public yellow_node
{
  public:
    orange_node();
    virtual ~orange_node();

    void poke();
    void poke_adjacent_oranges();
};

最後の機能は興味深いです。書き方は次のとおりです。

void orange_node::poke_adjacent_oranges()
{
    auto adj_nodes = get_adjacent_nodes();
    foreach(auto node, adj_nodes) {
        // In this case, typeid() and static_cast might be faster
        std::shared_ptr<orange_node> o_node = dynamic_cast<orange_node>(node);
        if (o_node) {
             o_node->poke();
        }
    }
}

これはすべて明確できれいに見えます。必要のない属性やメソッドを定義する必要はありません。ベースノードクラスは無駄のないままにすることができます。 RTTIがない場合、どこから始めますか?たぶん、node_type属性を基本クラスに追加できます。

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();

  private:
    std::string my_type;
};

Std :: stringは型に適したアイデアですか?そうではないかもしれませんが、他に何を使用できますか?番号を作成し、まだ誰も使用していないことを願っていますか?また、私のorange_nodeの場合、red_nodeとyellow_nodeのメソッドを使用したい場合はどうすればよいですか?ノードごとに複数のタイプを保存する必要がありますか?それは複雑に思えます。

結論

この例は、過度に複雑でも異常でもないように見えます(ノードはソフトウェアを介して制御される実際のハードウェアを表し、その内容に応じて非常に異なることを行うという、日々の仕事で同様の作業を行っています)。しかし、テンプレートまたは他のメソッドを使用してこれを行うクリーンな方法を知りません。私の例を擁護するのではなく、問題を理解しようとしていることに注意してください。 SO上記でリンクした回答と Wikibooksのこのページ などのページの私の読書は、RTTIを誤用していることを示唆しているようですが、その理由を知りたいと思います。

だから、元の質問に戻ります:なぜ「純粋な多型」がRTTIを使用するよりも望ましいのでしょうか?

104
mbr0wn

インターフェースは、コード内の特定の状況で対話するために知っておくべきことを記述します。 「型階層全体」でインターフェイスを拡張すると、インターフェイスの「表面積」が巨大になり、それについて推論することになりますharder

例として、あなたの「隣接するオレンジを突く」とは、第三者として私がオレンジであることをエミュレートできないことを意味します!オレンジ色の型を非公開で宣言した後、RTTIを使用して、その型と対話するときにコードが特別な動作をするようにします。 「オレンジ色」にしたい場合、私はmustプライベートガーデン内にいる必要があります。

これで、「オレンジ」とカップルするすべての人が、定義されたインターフェイスではなく、オレンジ型全体と暗黙のうちにプライベートガーデン全体とカップルします。

一見したところ、これはすべてのクライアントを変更せずに制限されたインターフェースを拡張する素晴らしい方法のように見えます(am_I_orange)、代わりに発生する傾向があるのは、コードベースを骨化し、preventsさらに拡張することです。特別なオレンジ色は、システムの機能に固有のものとなり、異なる方法で実装され、依存関係を削除したり、他の問題をエレガントに解決したりするオレンジの「タンジェリン」置換を作成できなくなります。

これは、インターフェースが問題を解決するのに十分でなければならないことを意味します。その観点から、なぜオレンジだけを突く必要があるのか​​、もしそうなら、なぜオレンジがインターフェースで利用できなかったのですか?アドホックに追加できるファジータグのセットが必要な場合は、タイプに追加できます。

class node_base {
  public:
    bool has_tag(tag_name);

これにより、狭いタグベースから狭いタグベースへのインターフェイスの同様の大規模な拡張が提供されます。 RTTIと実装の詳細(別名、「どのように実装されていますか?オレンジ色のタイプ?Ok you pass ..)異なる実装。

これは動的メソッドに拡張することもできますif必要です。 「Baz、Tom、Aliceの議論にうんざりすることを支持しますか?広い意味で、これはless動的キャストよりも侵入的であり、他のオブジェクトが既知のタイプであるという事実を取得します。

タンジェリンオブジェクトは、実装が分離された状態でオレンジ色のタグを付けて再生できるようになりました。

依然として大きな混乱を招く可能性がありますが、実装階層ではなく、少なくともメッセージとデータの混乱です。

抽象化は、非関連性を切り離して隠すゲームです。これにより、ローカルでコードを推論しやすくなります。 RTTIは、抽象化を介して実装の詳細に直接穴を開けています。これにより、問題の解決が容易になりますが、特定の実装に簡単にロックするコストがかかります。

これまたはその機能に対する道徳的説得の大部分は、誤解された使用のuが存在するという観察に由来する典型的なものですその機能。

モラリストが失敗するのは、すべての使用法が誤解されていると仮定している一方で、実際には機能が理由で存在していることです。

彼らは私が「配管工複合体」と呼んでいたものを持っています:彼らはすべてのtaps誤動作であると考えています修理します。現実には、ほとんどのタップはうまく機能します。単に配管工を呼ぶ必要はありません!

発生する可能性のあるおかしなことは、特定の機能の使用を避けるために、プログラマーが多くの定型コードを書いて、その機能を厳密に再実装することです。 (RTTIも仮想呼び出しも使用しないが、実際の派生型がどれであるかを追跡する値を持つクラスに出会ったことがありますか?[〜#〜] rtti [〜#〜 ]変装した再発明。

多態性について考える一般的な方法があります:IF(selection) CALL(something) WITH(parameters)。 (申し訳ありませんが、抽象化を無視する場合のプログラミングはすべてです)

設計時(概念)コンパイル時(テンプレート推論ベース)、実行時(継承および仮想関数ベース)、またはデータ駆動型(RTTIおよび切り替え)ポリモーフィズムの使用は、どの程度の決定が知られているかに依存します。 productionの各段階で、どのコンテキストでどのようにvariableであるか。

アイデアは次のとおりです。

予測できるほど、エラーをキャッチし、エンドユーザーに影響するバグを回避する可能性が高くなります。

すべてが一定である場合(データを含む)、テンプレートメタプログラミングですべてを実行できます。実際の定数でコンパイルが行われた後、program全体がresultを吐き出すreturnステートメントに要約されます。

コンパイル時にすべて既知のケースが多数あるが、それらが処理する必要がある実際のデータがわからない場合は、コンパイル時のポリモーフィズム(主にCRTPなど)が解決策となります。

ケースの選択がデータ(コンパイル時の既知の値ではない)に依存し、切り替えが1次元の場合(処理を1つの値に減らすことができる)、仮想関数ベースのディスパッチ(または一般に「関数ポインターテーブル」 ")が必要です。

切り替えが多次元の場合、C++にはネイティブ複数のランタイムディスパッチが存在しないため、次のいずれかを行う必要があります。

  • Goedelizationで1次元に縮小:それは、diamondsおよびstackedで仮想ベースと多重継承平行四辺形はありますが、これには、可能な組み合わせの数が既知であり、比較的小さいことが必要です。
  • 複合訪問者パターンのように、ディメンションを互いに連鎖させます(ただし、これはすべてのクラスが他の兄弟を認識する必要があるため、想像された場所から「スケールアウト」できません)
  • 複数の値に基づいて呼び出しをディスパッチします。 それがまさにRTTIの目的です

切り替えだけでなく、アクションでさえコンパイル時間がわからない場合は、スクリプティングと解析が必要です。データ自体はそれらに対して実行されるアクションを記述する必要があります。

さて、私が列挙した各ケースは、それに続く特定のケースと見なすことができるため、最上位で手頃な価格の問題についても最下位のソリューションを乱用することで、すべての問題を解決できます。

それが、道徳化が実際に回避するためにプッシュするものです。しかし、それは最下位のドメインに住んでいる問題が存在しないことを意味しません!

RTTIをバッシングするためにバッシングすることは、gotoをバッシングするためにバッシングするようなものです。プログラマではなく、オウムのためのもの。

31

小さな例ではちょっと見栄えが良いように見えますが、実際には、すぐにお互いを突くことができるタイプの長いセットになります。

_dark_orange_node_、または_black_and_orange_striped_node_、または_dotted_node_はどうですか?異なる色のドットを使用できますか?ほとんどのドットがオレンジ色の場合、それを突くことができますか?

そして、新しいルールを追加するたびに、すべての_poke_adjacent_関数を再確認し、さらにifステートメントを追加する必要があります。


いつものように、一般的な例を作成するのは難しいので、それを紹介します。

しかし、これを行う場合specificの例では、すべてのクラスにpoke()メンバを追加し、一部のクラスに呼び出しを無視させます(void poke() {})彼らが興味を持っていない場合。

確かに、それはtypeidsを比較するよりもさらに安価です。

23
Bo Persson

一部のコンパイラはそれを使用しません/ RTTIは常に有効ではありません

あなたはそのような議論を誤解していると思います。

RTTIを使用しないC++コーディングの場所がいくつかあります。 RTTIを強制的に無効にするためにコンパイラスイッチが使用される場合。このようなパラダイム内でコーディングしている場合は、ほぼ確実にすでにこの制限について通知されています。

したがって、問題はライブラリにあります。つまり、RTTIに依存するライブラリを作成している場合、RTTIをオフにするユーザーは、ライブラリcannotを使用します。ライブラリをそれらの人々が使用したい場合、RTTIを使用できる人々がライブラリを使用しても、RTTIは使用できません。同様に重要なことは、RTTIを使用できない場合、RTTIの使用は取引を妨げるものであるため、ライブラリを少し難しくする必要があることです。

メモリが余分にかかります/遅くなる可能性があります

ホットループではしないことが多くあります。メモリを割り当てません。リンクリストを繰り返し処理することはありません。などなど。 RTTIは確かに、「ここでこれをしない」ことのもう1つの可能性があります。

ただし、すべてのRTTIの例を検討してください。すべての場合において、1つ以上の不確定型のオブジェクトがあり、それらの一部に対しては不可能な操作を実行したい場合があります。

designレベルで回避する必要があるものです。 「STL」パラダイムに適合するメモリを割り当てないコンテナを書くことができます。リンクリストのデータ構造を回避したり、使用を制限したりできます。構造体の配列を配列の構造体などに再編成できます。それはいくつかのことを変更しますが、あなたはそれを区分化したままにすることができます。

複雑なRTTI操作を通常の仮想関数呼び出しに変更しますか?それは設計上の問題です。これを変更する必要がある場合は、every派生クラスの変更が必要です。さまざまなクラスと対話するコードの量を変更します。このような変更の範囲は、コードのパフォーマンスクリティカルなセクションをはるかに超えています。

それで...そもそもなぜ間違った方法で書いたのですか?

必要のない属性やメソッドを定義する必要はありません。ベースノードクラスは無駄のないままにすることができます。

何のために?

基本クラスは「無駄のない」と言います。しかし、本当に... nonexistentです。実際には何でもするではありません。

あなたの例を見てください:node_base。それは何ですか?隣接する他のものがあるようです。これはJavaインターフェース(pre-generics Java at at)):ユーザーがにキャストできるものとしてのみ存在するクラスreal type。隣接のような基本的な機能を追加するかもしれません(JavaはToStringを追加します)が、それだけです。

「無駄のない平均」と「透明」には違いがあります。

Yakkが言ったように、そのようなプログラミングスタイルは相互運用性を制限します。すべての機能が派生クラスにある場合、その派生クラスにアクセスできないシステム外のユーザーはシステムと相互運用できないためです。仮想関数をオーバーライドして、新しい動作を追加することはできません。 callこれらの関数さえもできません。

しかし、彼らが行うことは、システム内であっても、新しいことを実際に行うことを大きな苦痛にします。 poke_adjacent_oranges関数を検討してください。誰かがLime_nodesと同じように突くことができるorange_nodeタイプを望んでいる場合はどうなりますか?さて、Lime_nodeからorange_nodeを導出することはできません。それは意味がありません。

代わりに、Lime_nodeから派生した新しいnode_baseを追加する必要があります。次に、poke_adjacent_orangesの名前をpoke_adjacent_pokablesに変更します。そして、orange_nodeおよびLime_node;にキャストしてみてください。どのキャストが動作するかは、私たちが突くものです。

ただし、Lime_nodeにはownpoke_adjacent_pokablesが必要です。そして、この関数は同じキャストチェックを行う必要があります。

3番目の型を追加する場合、独自の関数を追加するだけでなく、他の2つのクラスの関数を変更する必要があります。

明らかに、poke_adjacent_pokablesを無料の関数にすると、すべての関数で機能します。しかし、誰かが4番目のタイプを追加し、それをその関数に追加するのを忘れた場合、どうなると思いますか?

こんにちは、サイレントブレーク。プログラムは多かれ少なかれ正常に動作するように見えますが、そうではありません。 pokeactual仮想関数であった場合、node_baseから純粋な仮想関数をオーバーライドしなかった場合、コンパイラは失敗します。

あなたの方法では、そのようなコンパイラチェックはありません。確かに、コンパイラは非純粋な仮想をチェックしませんが、少なくとも保護が可能な場合には保護があります(つまり、デフォルトの操作はありません)。

RTTIで透過的な基本クラスを使用すると、メンテナンスの悪夢につながります。実際、RTTIのほとんどの使用はメンテナンスの頭痛の種になります。これは、RTTIが有用でないことを意味するものではありません(たとえば、boost::anyを機能させるために不可欠です)。しかし、それはvery特殊なニーズのための非常に特殊なツールです。

そのように、それはgotoと同じように「有害」です。これは廃止すべきではない便利なツールです。ただし、コード内ではrareを使用する必要があります。


したがって、透明な基本クラスと動的なキャストを使用できない場合、太ったインターフェイスをどのように回避しますか?型で呼び出したいすべての関数が基本クラスまでバブリングしないようにするにはどうすればよいですか?

答えは、基本クラスの目的によって異なります。

node_baseなどの透過的な基本クラスは、問題に対して間違ったツールを使用しているだけです。リンクリストは、テンプレートによって最適に処理されます。ノードタイプと隣接関係は、テンプレートタイプによって提供されます。ポリモーフィック型をリストに入れたい場合は、できます。テンプレート引数でTとしてBaseClass*を使用するだけです。または、お好みのスマートポインター。

しかし、他のシナリオもあります。 1つは多くのことを実行するタイプですが、いくつかのオプション部分があります。特定のインスタンスは特定の機能を実装するかもしれませんが、別のインスタンスは実装しません。ただし、そのようなタイプの設計は通常、適切な答えを提供します。

「エンティティ」クラスは、この完璧な例です。このクラスは、ゲーム開発者を悩ませてきました。概念的には、巨大なインターフェースを備えており、ほぼ10のまったく異なるシステムの交差点に住んでいます。また、異なるエンティティには異なるプロパティがあります。一部のエンティティには視覚的な表現がないため、それらのレンダリング関数は何もしません。そして、これはすべて実行時に決定されます。

これに対する最新のソリューションは、コンポーネントスタイルのシステムです。 Entityは、コンポーネントのセットのコンテナであり、それらの間に接着剤があります。一部のコンポーネントはオプションです。視覚的な表現を持たないエンティティには、「グラフィックス」コンポーネントはありません。 AIのないエンティティには「コントローラー」コンポーネントがありません。などなど。

このようなシステムのエンティティは、コンポーネントへの単なるポインタであり、そのインターフェイスのほとんどは、コンポーネントに直接アクセスすることで提供されます。

このようなコンポーネントシステムを開発するには、設計段階で、特定の機能が概念的にグループ化されていることを認識する必要があります。これにより、予想される基本クラスからクラスを抽出し、別個のコンポーネントにすることができます。

これは、単一責任の原則に従うことにも役立ちます。このようなコンポーネント化されたクラスは、コンポーネントの所有者であるという責任のみを負います。


マシュー・ウォルトンから:

多くの答えは、node_baseがライブラリの一部であり、ユーザーが独自のノードタイプを作成することをあなたの例が示唆しているという考えに注意していないことに注意してください。その後、node_baseを変更して別のソリューションを許可することはできないため、RTTIが最適なオプションになる可能性があります。

OK、それを調べましょう。

これを理解するために必要なのは、一部のライブラリLがデータのコンテナーまたはその他の構造化されたホルダーを提供する状況です。ユーザーは、このコンテナにデータを追加したり、その内容を反復処理したりすることができます。ただし、ライブラリはこのデータに対して実際には何もしません。単にその存在を管理するだけです。

しかし、その存在はdestructionほど管理されていません。その理由は、そのような目的でRTTIを使用することが予想される場合、Lが無知であるクラスを作成しているからです。つまり、your codeはオブジェクトを割り当て、管理のためにLに渡します。

現在、このようなものが正当な設計である場合があります。イベントシグナル/メッセージパッシング、スレッドセーフワークキューなど。ここでの一般的なパターンは次のとおりです。誰かが任意のタイプに適した2つのコード間でサービスを実行していますが、サービスは特定のタイプを認識する必要はありません。

Cでは、このパターンのスペルはvoid*であり、その使用には、破損しないように細心の注意が必要です。 C++では、このパターンのスペルは std::experimental::any (間もなくstd::any)になります。

このoughtが機能する方法は、Lが実際のデータを表すanyを取るnode_baseクラスを提供することです。メッセージ、スレッドキューワークアイテム、またはあなたがしていることを受信したら、そのanyを適切なタイプにキャストします。これは送信者と受信者の両方が知っています。

したがって、orange_nodeからnode_dataを導出する代わりに、node_dataorangeメンバーフィールド内にanyを挿入するだけです。エンドユーザーはそれを抽出し、any_castを使用してorangeに変換します。キャストが失敗した場合、orangeではありませんでした。

anyの実装に慣れている場合は、「ちょっと待ってください:anyinternallyはRTTIを使用して、 any_cast動作します。」 「...はい」と答えます。

それがabstractionのポイントです。詳細については、誰かがRTTIを使用しています。しかし、あなたが操作すべきレベルでは、直接RTTIはあなたがすべきことではありません。

必要な機能を提供するタイプを使用する必要があります。結局のところ、RTTIは本当に必要ありません。必要なのは、指定された型の値を格納し、目的の宛先以外のすべてのユーザーからそれを非表示にしてから、その型に変換し直し、格納された値が実際にその型であることを確認できるデータ構造です。

それはanyと呼ばれます。 ses RTTIですが、anyを使用するほうが、RTTIを直接使用するよりもはるかに優れています。望ましいセマンティクスにより適合しているからです。

18
Nicol Bolas

関数を呼び出す場合、原則として、どのような正確なステップを実行するかはあまり気にしませんが、特定の制約内でいくつかのより高いレベルの目標が達成されることだけです(そして関数がそれをどのように実現するかは実際にはそれ自体の問題です)。

RTTIを使用して、特定のジョブを実行できる特殊なオブジェクトを事前選択し、同じセットの他のオブジェクトは実行できない場合、その快適な世界観を壊しています。突然、呼び出し側は、自分の手下にそれを実行するように単に伝えるのではなく、誰が何をすることができるかを知っているはずです。一部の人々はこれに悩まされており、これがRTTIが少し汚れていると考えられる理由の大部分だと思います。

パフォーマンスの問題はありますか?たぶん、しかし、私はそれを一度も経験したことがなく、20年前から、または2つではなく3つのアセンブリ命令を使用するのは受け入れがたい肥大化であると正直に信じる人々からの知恵かもしれません。

対処方法...状況に応じて、ノード固有のプロパティを個別のオブジェクトにバンドルすることが理にかなっている場合があります(つまり、「オレンジ」API全体が個別のオブジェクトになる場合があります)。次に、ルートオブジェクトに「オレンジ」APIを返す仮想関数を持たせ、オレンジ以外のオブジェクトに対してデフォルトでnullptrを返します。

これは状況によってはやり過ぎかもしれませんが、特定のノードが特定のAPIをサポートしているかどうかをルートレベルで照会し、サポートしている場合はそのAPIに固有の機能を実行できます。

10
H. Guijt

C++は、静的型チェックの概念に基づいて構築されています。

[1]RTTI、つまり、dynamic_castおよびtype_idは、動的な型チェックです。

したがって、本質的には、静的な型チェックが動的な型チェックよりも望ましい理由を尋ねています。そして、簡単な答えは、静的な型チェックが動的な型チェックよりも望ましいかどうかに依存しますです。たくさん。しかし、C++は、静的型チェックの概念に基づいて設計されたプログラミング言語の1つです。そしてこれは、例えば開発プロセス、特にテストは、通常、静的な型チェックに適合しており、最適なものに適合しています。


テンプレートまたは他のメソッドを使用してこれを行うクリーンな方法がわからない

このプロセス-異種ノードのグラフ-静的型チェックを使用して、ビジターパターンを介してキャストすることはできません。このような:

#include <iostream>
#include <set>
#include <initializer_list>

namespace graph {
    using std::set;

    class Red_thing;
    class Yellow_thing;
    class Orange_thing;

    struct Callback
    {
        virtual void handle( Red_thing& ) {}
        virtual void handle( Yellow_thing& ) {}
        virtual void handle( Orange_thing& ) {}
    };

    class Node
    {
    private:
        set<Node*> connected_;

    public:
        virtual void call( Callback& cb ) = 0;

        void connect_to( Node* p_other )
        {
            connected_.insert( p_other );
        }

        void call_on_connected( Callback& cb )
        {
            for( auto const p : connected_ ) { p->call( cb ); }
        }

        virtual ~Node(){}
    };

    class Red_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        auto redness() -> int { return 255; }
    };

    class Yellow_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }
    };

    class Orange_thing
        : public Red_thing
        , public Yellow_thing
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        void poke() { std::cout << "Poked!\n"; }

        void poke_connected_orange_things()
        {
            struct Poker: Callback
            {
                void handle( Orange_thing& obj ) override
                {
                    obj.poke();
                }
            } poker;

            call_on_connected( poker );
        }
    };
}  // namespace graph

auto main() -> int
{
    using namespace graph;

    Red_thing   r;
    Yellow_thing    y1, y2;
    Orange_thing    o1, o2, o3;

    for( Node* p : std::initializer_list<Node*>{ &y1, &y2, &r, &o2, &o3 } )
    {
        o1.connect_to( p );
    }
    o1.poke_connected_orange_things();
}

これは、ノードタイプのセットが既知であることを前提としています。

そうでない場合は、ビジターパターン(多くのバリエーションがあります)は、少数の集中型キャスト、または単一のキャストで表現できます。


テンプレートベースのアプローチについては、Boost Graphライブラリを参照してください。悲しいことに、私はそれに慣れていない、私はそれを使用していません。だから、それが何をどのように、そしてどの程度、RTTIの代わりに静的型チェックを使用するのか正確にはわかりませんが、Boostは一般的に静的型チェックを中心としたテンプレートベースなので、あなたはそれを見つけると思いますGraphサブライブラリも静的型チェックに基づいています。


[1] 実行時タイプ情報

もちろん、ポリモーフィズムが役に立たないシナリオがあります:名前。 typeidを使用すると、型の名前にアクセスできますが、この名前のエンコード方法は実装定義です。ただし、通常は2つのtypeid- sを比較できるため、これは問題になりません。

if ( typeid(5) == "int" )
    // may be false

if ( typeid(5) == typeid(int) )
   // always true

ハッシュについても同様です。

[...] RTTIは「有害と見なされる」

harmfulは間違いなく誇張されています。RTTIにはいくつかの欠点がありますが、doesにも利点があります。

本当にRTTIを使用する必要はありません。 RTTIはtoolで解決しますOOP問題:別のパラダイムを使用すると、これらはおそらく消えます。CにはRTTIはありませんが、 C++は代わりにOOPを完全にサポートし、ランタイム情報を必要とする可能性のある問題を克服するためのmultipleツールを提供します:それらの1つis確かにRTTIですが、価格が付いていますが、余裕がない場合は、安全なパフォーマンス分析を行った後にのみ述べたほうがよいでしょう。古い学校void*:まだ無料です。コストはかかりませんが、タイプセーフはありません。


  • 一部のコンパイラは使用しません/ RTTIは常に有効ではありません
    私は本当にこの議論を買いません。それは、C++ 14の機能を使用すべきではないと言っているようなものです。C++ 14の機能をサポートしていないコンパイラがあるからです。それでも、C++ 14の機能を使うことを思いとどまらせる人はいません。

(おそらく厳密に)準拠するC++コードを記述する場合、実装に関係なく同じ動作を期待できます。標準準拠の実装は、標準のC++機能をサポートするものとします。

ただし、一部の環境ではC++が定義している("独立型の)環境では、RTTIを提供する必要はなく、例外、virtualなども提供しないことを考慮してください。 RTTIには、ABIや実際の型情報などの低レベルの詳細を処理する、正しく機能するための基礎となる層が必要です。


この場合のRTTIに関するYakkに同意します。はい、使用できます。しかし、それは論理的に正しいですか?言語がこのチェックをバイパスすることを許可するという事実は、それが行われるべきであることを意味しません。

3
edmz