13

これが私が話していることです

// some guy wrote this, used as a Policy with templates
struct MyWriter {
  void write(std::vector<char> const& data) {
    // ...
  }
};

一部の既存のコードでは、人々はテンプレートを使用しませんでしたが、インターフェイス + 型消去を使用しました

class IWriter {
public:
  virtual ~IWriter() {}

public:
  virtual void write(std::vector<char> const& data) = 0;
};

他の誰かが、アプローチと書き込みの両方で使用できるようにしたいと考えていました

class MyOwnClass: private MyWriter, public IWriter {
  // other stuff
};

MyOwnClass は、MyWriter の観点から実装されています。MyOwnClass の継承メンバー関数が IWriter のインターフェイスを自動的に実装しないのはなぜですか? 代わりに、ユーザーは次のように、基本クラスのバージョンを呼び出すだけの転送関数を作成する必要があります。

class MyOwnClass: private MyWriter, public IWriter {
public:
  void write(std::vector<char> const& data) {
    MyWriter::write(data);
  }
};

Java では、インターフェイスを実装し、たまたま適切なメソッドを持つクラスから派生するクラスがある場合、その基本クラスは派生クラスのインターフェイスを自動的に実装することを知っています。

なぜC++はそれをしないのですか? 持つのは当たり前のようです。

4

5 に答える 5

15

これは多重継承であり、同じ署名を持つ 2 つの継承された関数があり、どちらも implementation を持っています。そこが C++ と Java の違いです。

したがってwrite、静的型が である式を呼び出すと、継承された関数のどれが必要かが曖昧になります。MyBigClass

writeが基本クラスのポインターを介してのみ呼び出される場合write、質問の主張に反して、派生クラスで定義する必要はありません。 質問が純粋な指定子を含むように変更されたので、クラスを具体的かつインスタンス化できるようにするには、派生クラスにその関数を実装する必要があります。

MyWriter::writeの仮想呼び出しメカニズムには使用できません。仮想呼び出しメカニズムには、暗黙的な を受け入れる関数と、暗黙的なをMyBigClass受け入れる関数が必要なためです。サブオブジェクトとサブオブジェクトのアドレスの違いを考慮に入れる必要がある新しい関数が必要です。IWriter* const thisMyWriter::writeMyWriter* const thisIWriterMyWriter

コンパイラがこの新しい関数を自動的に作成することは理論的には可能ですが、基本クラスの変更により突然新しい関数が転送用に選択される可能性があるため、脆弱です。単一の継承のみが可能な Java ではそれほど脆弱ではありませんが (どの関数に転送するかの選択肢は 1 つしかありません)、多重継承を完全にサポートする C++ では、その選択はあいまいであり、ダイヤモンドの継承についてはまだ開始していません。または仮想継承はまだ。

実際、この問題 (サブオブジェクトのアドレスの違い) は、仮想継承によって解決されます。しかし、ほとんどの場合必要ではない追加のオーバーヘッドが必要であり、C++ の指針となる原則は、「使用しないものに対して料金を支払う必要はない」です。

于 2012-05-05T17:46:49.257 に答える
5

なぜC++はそれをしないのですか? 持つのは当たり前のようです。

実は、いや、非常に不自然な存在です。

私の推論は、私自身の「常識」の理解に基づいており、結果として根本的に欠陥がある可能性があることに注意してください。

ご覧のとおり、2 つの異なるメソッドがあります。1 つ目は MyWriter の非仮想メソッドで、2 つ目は IWriter の仮想メソッドです。「似ている」にもかかわらず、それらは完全に異なります。

この質問を確認することをお勧めします。非仮想メソッドの良いところは、仮想メソッドを呼び出さない限り、何をしても動作が変わらないことです。つまり、非仮想メソッドを使用してクラスから派生した誰かが、既存のメソッドをマスクして壊すことはありません。仮想メソッドはオーバーライドされるように設計されています。その代償は、仮想メソッドを不適切にオーバーライドすることで、基になるロジックを壊す可能性があることです。そして、これがあなたの問題の根源です。

あなたが提案したものが許可されたとしましょう。(多重継承による virtual への自動変換) 考えられる解決策は 2 つあります。

解決策 #1 MyWriter が仮想になります。結果: 世界中のすべての既存の C++ コードは、タイプミスや名前の衝突によって壊れやすくなります。MyWriter メソッドは最初はオーバーライドされることは想定されていなかったため、突然仮想に変更すると (マーフィーの法則)、誰かが MyOwnClass から派生したときに MyWriter クラスの基になるロジックが壊れます。つまり、突然 MyWriter::write を仮想化するのは悪い考えです。

解決策 #2 MyWriter は静的なままですが、オーバーライドされるまで仮想メソッドとして IWriter に一時的に含まれます。一見何も心配することはありませんが、考えてみましょう。IWriter は、あなたが念頭に置いていたある種の概念を実装しており、何かを行うことになっています。MyWriter は別の概念を実装しています。MyWriter::write を IWriter::write メソッドとして割り当てるには、次の 2 つの保証が必要です。

  1. コンパイラは、MyWriter::write が IWriter::write() が行うべきことを行うようにする必要があります。
  2. コンパイラは、IWriter から MyWriter::write を呼び出しても、プログラマが他の場所で使用する予定の MyWriter コードの既存の機能を壊さないようにする必要があります。

つまり、問題は、コンパイラがそれを保証できないということです。関数の名前と引数リストは似ていますが、マーフィーの法則により、関数はおそらくまったく異なることを行っていることになります。(たとえば、sinf と cosf は同じ引数リストを持ちます)、コンパイラが将来を予測して、開発のどの時点でも MyWriter が IWriter と互換性がなくなるような方法で変更されないことを確認できる可能性は低いです。つまり、機械はそれ自体で合理的な決定を下すことができないため (そのための AI はありません)、プログラマーであるあなたに「何をしたいのですか?」と尋ねる必要があります。そして、「仮想メソッドを MyWriter::write() にリダイレクトします。まったく何も壊れません。私は思います。」と言います。

そのため、使用する方法を手動で指定する必要があります....

于 2012-05-05T22:47:45.490 に答える
4

それを自動的に行うのは直感的ではなく、驚くべきことです。C++ は、複数の基本クラスが相互に関連しているとは想定せず、非静的メンバーのネストされた名前指定子を定義することにより、メンバー間の名前の衝突からユーザーを保護します。MyOwnClass署名が衝突する場所に暗黙の宣言を追加することはIWriterMyWriter名前を保護することとは正反対です。

ただし、C++11 の拡張機能は、私たちを近づけてくれます。このことを考慮:

class MyOwnClass: private MyWriter, public IWriter {
public:
  void write(std::vector<char> const& data) final = MyWriter::write;
};

MyWriterこのメカニズムは、それ以上のオーバーライドを期待しないことを表現するため安全であり、「結合」される関数シグネチャに名前を付けるだけでそれ以上のものではないため便利です。また、final関数が暗黙的でない場合は形式が正しくないvirtualため、署名が仮想インターフェイスと一致することを確認します。

一方では、ほとんどのインターフェイスがたまたまこのように一致するわけではありません。この機能を同一の署名でのみ機能するように定義することは安全ですが、ほとんど役に立ちません。デリゲート関数本体へのショートカットとして定義すると便利ですが、脆弱です。だから、それは本当に良い機能ではないかもしれません

一方、これは、必要のないときに仮想的ではない機能を提供するための優れた設計パターンです。したがって、このイディオムを考えると、現在の慣行とうまく一致しなくても、それを使用して優れたコードを書くことができます。

于 2012-05-06T09:20:01.263 に答える
3

なぜC++はそれをしないのですか?

ここで何を尋ねているのかわかりません。これを可能にするために C++ を書き直すことはできますか? はい、でも何のために?

MyWriterとは完全に異なるクラスであるためIWriter、C++ ではMyWriterのインスタンスを介してのメンバーを呼び出すことはできませんIWriter。メンバー ポインターの型はまったく異なります。そして、 a が a に変換できないのと同様に、 aも a に変換MyWriter*できません。IWriter*void (MyWriter::*)(const std::vector<char>&)void (IWriter::*)(const std::vector<char>&)

2 つを組み合わせた 3 番目のクラスが存在する可能性があるからといって、C++ の規則が変わるわけではありません。どちらのクラスも、互いに直接の親子関係ではありません。したがって、それらは完全に異なるクラスとして扱われます。

覚えておいてください: メンバー関数は常に追加のパラメーターを取ります:thisそれらが指すオブジェクトへのポインターです。void (MyWriter::*)(const std::vector<char>&)を呼び出すことはできませんIWriter*。3 番目のクラスには、自身を適切な基本クラスにキャストするメソッドを含めることができますが、実際にはこのメソッドが必要です。そのため、ユーザーまたは C++ コンパイラが作成する必要があります。C++ の規則ではこれが必要です。

派生クラスのメソッドを使用せずにこれを機能させるにはどうすればよいかを考えてみてください。

関数は を取得しIWriter*ます。ユーザーは、ポインターwriteだけを使用して、そのメンバーを呼び出します。IWriter*では...コンパイラはどのようにして呼び出すコードを生成できるのMyWriter::writerでしょうか? 注意:インスタンスMyWriter::writer が必要です。そして、 と の間にMyWriterは何の関係もありません。IWriterMyWriter

では、コンパイラはどのようにして型強制をローカルで行うことができるのでしょうか? コンパイラは、仮想関数をチェックして、呼び出される実際の関数がIWriter他の型を取るかどうかを確認する必要があります。別の型を取る場合は、ポインターを真の型に変換してから、仮想関数が必要とする型に別の変換を行う必要があります。それをすべて行った後、電話をかけることができます。

このオーバーヘッドはすべて、すべての仮想呼び出しに影響ます。それらはすべて、実際の関数が呼び出されるかどうかを少なくとも確認する必要があります。念のため、すべての呼び出しで型変換を行うコードを生成する必要もあります。

すべての仮想関数呼び出しには、「get type」と条件分岐が含まれます。そのブランチをトリガーすることが決して不可能であっても。そのため、使用するかどうかに関係なく、何かに対して料金を支払うことになります。それは C++ のやり方ではありません。

さらに悪いことに、仮想呼び出しの単純な v-table 実装はもはや不可能です。仮想ディスパッチを行う最速の方法は、適合する実装ではありません。C++ 委員会は、そのような実装を不可能にするような変更を行うつもりはありません。

繰り返しますが、何のために?簡単な転送関数を書く必要がないように?

于 2012-05-05T20:41:38.547 に答える
-2

MyWriter を IWriter から派生させ、MyOwnClass 内の IWriter 派生を削除して、先に進みます。これにより、問題が解決され、テンプレート コードが妨げられることはありません。

于 2012-05-05T18:51:53.253 に答える