RTTIを使用するよりも「純粋なポリモーフィズム」が望ましいのはなぜですか?


107

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

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

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

-fno-rtti

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

これは、RTTIを使用していない場合は無効にできることを示しています。つまり、Boostを使用していない場合は、Boostにリンクする必要はありません。誰かがでコンパイルする場合を想定する必要はありません-fno-rtti。さらに、この場合、コンパイラは大音量でクリアします。

追加のメモリが必要/遅くなる可能性がある

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

実際、dynamic_castは遅くなる可能性があります。ただし、スピードが重要な状況での使用を回避する方法は通常いくつかあります。そして、私は代替案をまったく見ていません。このSOの回答は、基本クラスで定義された列挙型を使用して型を格納することを提案しています。これは、すべての派生クラスを事前に知っている場合にのみ機能します。それはかなり大きな「もし」だ!

その回答から、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を使用するよりも「純粋なポリモーフィズム」が好ましいのはなぜですか?


9
ポークオレンジの例を解決するために(言語機能として)「欠けている」のは、複数のディスパッチ(「マルチメソッド」)です。したがって、それをエミュレートする方法を探すことが代替手段になる可能性があります。したがって、通常はビジターパターンが使用されます。
Daniel Jour

1
タイプとして文字列を使用することはあまり役に立ちません。いくつかの「タイプ」クラスのインスタンスへのポインタを使用すると、これが速くなります。しかし、基本的には、RTTIが行うことを手動で行っています。
Daniel Jour

4
@MargaretBloomいいえ、違います。RTTPはランタイムタイプ情報を表しますが、CRTPはテンプレート(静的タイプ)専用です。
edmz 2016

2
@ mbr0wn:すべてのエンジニアリングプロセスはいくつかのルールに拘束されます。プログラミングも例外ではありません。ルールは、ソフトルール(SHOULD)とハードルール(MUST)の2つのバケットに分割できます。(いわば、アドバイス/オプションバケット(COULD)もあります。)C / C ++標準(または実際には他の英語標準)がそれらをどのように定義するかを読んでください。あなたの問題は、「RTTIを使用しない」をハードと間違えたという事実から来ていると思いますルールと誤解しているためだと思います(「」)。これは実際にはソフトルール(「RTTIを使用しないでください」)です。つまり、可能な限り回避し、回避できない場合にのみ使用

3
多くの回答は、あなたの例がnode_baseライブラリの一部であり、ユーザーが独自のノードタイプを作成することを示唆しているという考えに注意していないことに注意してください。それから彼らはnode_base別のソリューションを許可するように変更することはできないため、おそらくRTTIが最良のオプションになるでしょう。一方、RTTIを使用せずに新しいノードタイプがよりエレガントに収まるように、このようなライブラリを設計する方法は他にもあります(他の方法で新しいノードタイプを設計することもできます)。
マシューウォルトン

回答:


69

インターフェイスは、コード内の特定の状況で対話するために知る必要があることを記述します。「タイプ階層全体」でインターフェースを拡張すると、インターフェースの「表面領域」が巨大になり、それを推論することが難しくなります。

例として、「隣接するオレンジを突く」とは、サードパーティとしてオレンジであることをエミュレートできないことを意味します。オレンジ型を非公開で宣言し、RTTIを使用して、その型とやり取りするときにコードを特別な動作にする。「オレンジ色」になりたいのなら、私あなたのプライベートガーデンの中にいるに違いありません

これで、「オレンジ色」と連動するすべての人が、定義されたインターフェイスではなく、オレンジタイプ全体とプライベートガーデン全体と連動します。

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

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

class node_base {
  public:
    bool has_tag(tag_name);

これにより、狭義の仕様から広義のタグベースまで、同様の大規模なインターフェースの拡張が実現します。RTTIと実装の詳細(別名、「実装方法は?オレンジ色のタイプですか?合格です。」)を使用する代わりに、完全に異なる実装を介して簡単にエミュレートされるものを使用します。

必要であれば、これを動的メソッドに拡張することもできます。「あなたはバズ、トム、アリスの議論でフーされていることを支持しますか?大きな意味で、これはあまり実際に他のオブジェクトを取得するための動的キャストは、あなたが知っているタイプであるよりも、押し付けがましいです。

これでタンジェリンオブジェクトはオレンジ色のタグを付けて一緒に再生できますが、実装は分離されています。

それでも巨大な混乱につながる可能性がありますが、実装階層ではなく、少なくともメッセージとデータの混乱です。

抽象化は、無関係を切り離し、隠すゲームです。ローカルでコードを推論するのが簡単になります。RTTIは、抽象化を通して実装の詳細に直接穴を開けています。これにより、問題の解決が容易になりますが、特定の実装に簡単に固定できるというコストがかかります。


14
最後の段落の+1。私があなたに同意するからだけでなく、それはここで釘を打つハンマーだからです。

7
オブジェクトがその機能をサポートするものとしてタグ付けされていることを知った後、どのようにして特定の機能に到達しますか?これにはキャストが含まれるか、すべての可能なメンバー関数を持つGodクラスがあります。最初の可能性は、チェックされていないキャストです。この場合、タグ付けは独自の非常に誤りのある動的型チェックスキームであるか、チェックされているdynamic_cast(RTTI)場合です。この場合、タグは冗長です。2番目の可能性、神のクラスは忌まわしいです。要約すると、この答えにはJavaプログラマーにはいいと思う単語がたくさんありますが、実際の内容は意味がありません。
乾杯とhth。-Alf

2
@ファルコ:これは、タグに基づいた未チェックのキャストで、私が言及した最初の可能性(の1つの変形)です。ここでのタグ付けは、独自の非常に壊れやすく、非常に誤りやすい動的型チェック方式です。小さなクライアントコードは誤動作し、C ++ではUBランドでは動作しません。Javaのように例外は発生しませんが、クラッシュなどの未定義の動作や不正な結果が発生します。非常に信頼性が低く危険であることに加えて、より健全なC ++コードと比較すると、非常に非効率的です。IOW。、それは非常に良くない。非常にそうです。
乾杯とhth。-Alf

1
うーん。:)引数のタイプ?
乾杯とhth。-Alf

2
@JojOatXGME:「ポリモーフィズム」とは、さまざまなタイプで作業できることを意味するためです。それが特定の型であるかどうかを確認する必要がある場合は、ポインター/参照を最初に取得するために使用した既存の型チェックに加えて、多態性の背後にあることになります。さまざまなタイプで作業しているわけではありません。特定のタイプで作業しています。はい、これを行う「Javaの(大きな)プロジェクト」があります。しかし、それはJavaです。言語は動的なポリモーフィズムのみを許可します。C ++にも静的ポリモーフィズムがあります。また、誰かが「大きな」ことをするからといって、それが良い考えになるわけではありません。
Nicol Bolas

31

これまたはその機能に対する道徳的説得のほとんどは典型的なものであり、その機能の誤解された使用の数が少ないという観察に由来します。

道徳主義者が失敗するところは、実際には機能が理由のために存在する一方、すべての使用法が誤解されていると推定していることです。

彼らは私が「配管工コンプレックス」と呼んでいたものを持っています。修理のために呼び出されたすべてのタップ故障しているため、すべてのタップ誤動作していると考えています。現実には、ほとんどのタップが適切に機能します。単にタップを配管工と呼ばないでください。

クレイジーなことは、特定の機能の使用を回避するために、プログラマーが実際にその機能を正確に非公開で再実装する多くのボイラープレートコードを書くことです。(RTTIも仮想呼び出しも使用していないが、実際の派生型を追跡するための値を持っているクラスに出会ったことはありますか?これは偽装されたRTTIの再発明にすぎません。)

ポリモーフィズムについて考える一般的な方法がありますIF(selection) CALL(something) WITH(parameters)。(申し訳ありませんが、抽象化を無視する場合のプログラミングはそれだけです)

設計時(概念)コンパイル時(テンプレート推論ベース)、実行時(継承および仮想関数ベース)、またはデータ駆動型(RTTIおよびスイッチング)ポリモーフィズムの使用は、既知の決定の量に依存します。制作の各段階で、あらゆる状況でそれらがどのように変化するか。

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

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

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

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

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

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

  • Goedelizationによって1次元に削減します。これは、ダイヤモンド積み上げ平行四辺形を使用した仮想ベースと多重継承ですが、これには、可能な組み合わせの数がわかっていて比較的小さいことが必要です。
  • ディメンションを1つからもう1つにチェーンします(composite-visitorsパターンと同様ですが、これには、すべてのクラスが他の兄弟を認識する必要があるため、想定された場所から「スケールアウト」できません)。
  • 複数の値に基づくディスパッチ呼び出し。それがまさにRTTIの目的です。

切り替えだけでなく、アクションもコンパイル時間が不明な場合は、スクリプト作成と解析が必要です。データ自体が、実行するアクションを記述している必要があります。

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

それこそが、道徳化が実際に避けようとしていることです。しかし、それは最下位のドメインに存在する問題が存在しないことを意味しません!

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


各アプローチが適用されるレベルの適切な説明。「Goedelization」について聞いたことがありません-他の名前でも知られていますか?リンクや説明を追加できますか?ありがとう:)
j_random_hacker 2016

1
@j_random_hacker:私もこのGodelizationの使用に興味があります。通常、Godelizationは最初に文字列から整数にマッピングし、次にこの手法を使用して形式言語で自己参照ステートメントを生成するものと見なします。仮想ディスパッチのコンテキストではこの用語に慣れていないので、もっと詳しく知りたいと思います。
Eric Lippert、2016年

1
実際、私は用語を乱用しています:Goedleによれば、すべての整数は整数n-ple(その素因数の累乗)に対応し、すべてのn-pleは整数に対応するため、すべての離散n次元インデックス付け問題は一次元に縮小されます。これは、これが唯一の方法であることを意味するものではありません。「可能」と言うだけの方法です。必要なのは「分割統治」メカニズムだけです。仮想関数は「分割」であり、多重継承は「征服」です。
エミリオガラヴァリア2016年

... 有限体(範囲)内で起こるすべての場合、線形結合の方が効果的です(従来のi = r * C + cは、行列のセルの配列のインデックスを取得します)。この場合、分割IDは「訪問者」であり、征服者は「複合」です。線形代数が含まれるため、この場合の手法は「対角化」に対応します
Emilio Garavaglia

これらすべてをテクニックとして考えないでください。それらは単なる類似点です
エミリオガラヴァリア2016年

23

小さな例ではそれは一種の見栄えのように見えますが、実際には、お互いを突くことができるタイプの長いセットにすぐに行き着くでしょう、それらのいくつかはおそらく一方向にだけです。

何についてdark_orange_node、またはblack_and_orange_striped_node、またはdotted_node?異なる色のドットを使用できますか?ほとんどのドットがオレンジ色の場合、それを突くことができますか?

また、新しいルールを追加する必要があるたびに、すべてのpoke_adjacent関数に再度アクセスして、ifステートメントを追加する必要があります。


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

しかし、この特定の例を実行する場合、poke()すべてのクラスにメンバーを追加し、void poke() {}興味のない場合は一部のクラスに呼び出し()を無視させます。

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


3
あなたは「きっと」と言いますが、何があなたをそんなに確かにしているのですか?それが本当に私が理解しようとしていることです。たとえば、orange_nodeの名前をpokable_nodeに変更し、poke()を呼び出すことができるのはそれらだけだとします。つまり、私のインターフェースは、たとえば例外をスローするpoke()メソッドを実装する必要があることを意味します(「このノードはポークできません」)。それはもっと高いようです。
mbr0wn 2016

2
なぜ例外をスローする必要があるのでしょうか?インターフェースが「ポーク可能」かどうかを気にする場合は、「isPokeable」関数を追加して、それを呼び出してからポーク関数を呼び出してください。または、単に彼の言うことを実行し、「ポークできないクラスでは何もしない」。
Brandon

1
@ mbr0wn:より良い質問は、ポーク可能なノードとポークできないノードが同じ基本クラスを共有する理由です。
Nicol Bolas

2
@NicolBolas友好的で敵対的なモンスターが同じ基本クラスを共有したり、フォーカス可能なUI要素やフォーカスできないUI要素、またはテンキーのあるキーボードとテンキーのないキーボードを共有したりするのはなぜですか?
user253751 2016年

1
@ mbr0wnこれは動作パターンのように聞こえます。基本インターフェースには2つのメソッドがsupportsBehaviourありinvokeBehaviour、各クラスは動作のリストを持つことができます。1つの動作はPokeであり、ポーク可能にするすべてのクラスによって、サポートされる動作のリストに追加できます。
Falco

20

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

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

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

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

追加のメモリが必要/遅くなる可能性がある

ホットループではやらないことはたくさんあります。メモリを割り当てません。リンクされたリストを反復処理する必要はありません。などなど。RTTIは、「ここではこれを行わない」ということの1つになり得ます。

ただし、RTTIの例はすべて検討してください。すべての場合において、不確定タイプの1つ以上のオブジェクトがあり、それらの一部では実行できない可能性があるいくつかの操作を実行したいと考えています。

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

複雑なRTTI操作を通常の仮想関数呼び出しに変更しますか?それは設計上の問題です。それを変更する必要がある場合、それはすべてに変更を必要とするものです派生クラスを。多くのコードがさまざまなクラスとやり取りする方法を変更します。このような変更の範囲は、コードのパフォーマンスが重要なセクションをはるかに超えています。

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

属性やメソッドを必要としない場所で定義する必要はありません。基本ノードクラスは無駄のない状態を保つことができます。

何のために?

あなたは、基本クラスは「無駄のない」と言っています。しかし、本当に...それは存在しません。実際には何もしません。

あなたの例を見てください:node_base。それは何ですか?他に隣接しているもののようです。これはJavaインターフェースです(その前のジェネリックJava):ユーザーが実際の型にキャストできるものとしてのみ存在するクラス。おそらく、隣接(Javaの追加ToString)などの基本的な機能を追加しますが、それだけです。

「リーンと平均」と「透明」には違いがあります。

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

しかし、彼らがまたやっていることは、システム内でさえ、実際に新しいことを行うことを大きな苦痛にすることです。poke_adjacent_oranges関数を検討してください。誰かがsのlime_nodeように突くことができるタイプが必要な場合はどうなりorange_nodeますか?まあ、私たちはlime_nodeから派生することはできませんorange_node。それは意味がありません。

代わりに、lime_nodeから派生した新しいを追加する必要がありnode_baseます。次に、の名前をpoke_adjacent_orangesに変更しpoke_adjacent_pokablesます。そして、型にキャストしてみてくださいorange_nodelime_node。キャスト作品はどれでも私たちがつついたものです。

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

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

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

こんにちは、静かな破損。プログラムは多かれ少なかれ正常に動作するように見えますが、そうではありません。していたpokeとなって、実際の仮想関数、コンパイラはあなたがから純粋仮想関数をオーバーライドしていない時に失敗しただろうnode_base

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

RTTIで透過ベースクラスを使用すると、メンテナンスの悪夢につながります。実際、RTTIのほとんどの使用は、メンテナンスの頭痛を引き起こします。これは、RTTIが役に立たないことを意味するものではありません(boost::anyたとえば、作業を行うために不可欠です)。しかし、それは非常に特殊なニーズのための非常に特殊なツールです。

そのように、それはと同じように「有害」gotoです。これは、なくしてはならない便利なツールです。ただし、コード内で使用することはまれです。


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

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

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

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

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

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

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

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

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


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

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

OK、それを探検しましょう。

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

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

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

Cでは、このパターンは綴られてvoid*おり、その使用は、壊れないように細心の注意が必要です。C ++では、このパターンはスペルされますstd::experimental::any(まもなくスペルされますstd::any)。

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

したがって、から派生するのでorange_nodeはなく、node_data単にのメンバーフィールドのorange内側を貼り付けます。エンドユーザーはそれを抽出し、を使用してに変換します。キャストが失敗した場合は、そうではありませんでした。node_dataanyany_castorangeorange

ここで、の実装にまったく精通しているany場合は、「ちょっと待って:any 内部で RTTIを使用してany_cast機能する」と言うでしょう。私はそれに答えます、「...はい」。

それが抽象化のポイントです。詳細の深いところで、誰かがRTTIを使用しています。しかし、本来あるべきレベルでは、直接RTTIは実行すべきものではありません。

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

それがanyです。これ RTTI anyを使用しますが、目的のセマンティクスにより正確に適合するため、RTTIを直接使用するよりもはるかに優れています。


10

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

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

パフォーマンスの問題はありますか?多分、私はそれを経験したことがなく、20年前から、または2つではなく3つのアセンブリ命令を使用することは許容できない膨らみであると正直に信じている人々からの知恵かもしれません。

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

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


6
再:パフォーマンスコスト-私はdynamic_cast <>を測定したところ、3GHzプロセッサ上のアプリで約2µsかかりました。これは、列挙型のチェックよりも約1000倍遅いです。(私たちのアプリには11.1msのメインループの期限があるため、マイクロ秒に非常に注意を払っています。)
Crashworks

6
パフォーマンスは実装によって大きく異なります。GCCは、高速なtypeinfoポインター比較を使用します。MSVCは、高速ではない文字列比較を使用します。ただし、MSVCのメソッドは、静的またはDLLの異なるバージョンのライブラリにリンクされたコードで機能します。GCCのポインターメソッドは、静的ライブラリのクラスが共有ライブラリのクラスとは異なると見なします。
Zan Lynx

1
@Crashworksここに完全な記録があるだけです。それはどのコンパイラ(およびどのバージョン)でしたか?
H.ギジット2016年

@Crashworksは、どのコンパイラがあなたの観察した結果を生み出したかについての情報を求める要求を二次化します。ありがとう。
underscore_d

@underscore_d:MSVC。
Crashworks

9

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();
}

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

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


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


[1] 実行時タイプ情報


1
ビジターパターンに必要なコード(タイプを追加するときの変更)の量を減らすには、RTTIを使用して階層を「登る」ことにより、「面白いこと」が1つあります。これは「非循環ビジターパターン」として知られています。
Daniel Jour

3

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

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

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

ハッシュについても同じことが言えます。

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

有害は間違いなく誇張さ:RTTIは、いくつかの欠点を持っていますが、それはありませんあまりにも利点があります。

本当にRTTIを使用する必要はありません。RTTIはツールです OOPの問題を解決するための。別のパラダイムを使用すると、これらはおそらくなくなるでしょう。CにはRTTIがありませんが、動作します。C ++は代わりにOOPを完全にサポートし、ランタイム情報を必要とする可能性があるいくつかの問題を克服するための複数のツールを提供します。それらの1つ確かにRTTIですが、代償が伴います。あなたがそれを買う余裕がないならば、あなたが安全なパフォーマンス分析の後にのみあなたが述べたほうがいいということは、古い学校がまだありvoid*ます:それは無料です。費用はかかりません。しかし、型安全性はありません。だから、それはすべて取引についてです。


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

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

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


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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.