26

から継承したいのですstd::mapが、私が知る限り、std::map仮想デストラクタはありません。

std::mapしたがって、デストラクタで のデストラクタを明示的に呼び出して、適切なオブジェクトの破棄を保証することは可能ですか?

4

4 に答える 4

35

デストラクタは仮想でなくても呼び出されますが、それは問題ではありません。

へのポインタを介してタイプのオブジェクトを削除しようとすると、未定義の動作が発生しますstd::map

継承の代わりにコンポジションを使用してください。stdコンテナは継承されることを意図しておらず、そうすべきではありません。

の機能を拡張したいstd::map(たとえば、最小値を見つけたい) と仮定しています。その場合、はるかに優れた合法的なオプションが 2 つあります。

1) 提案されているように、代わりにコンポジションを使用できます。

template<class K, class V>
class MyMap
{
    std::map<K,V> m;
    //wrapper methods
    V getMin();
};

2) 無料機能:

namespace MapFunctionality
{
    template<class K, class V>
    V getMin(const std::map<K,V> m);
}
于 2012-05-07T06:48:26.573 に答える
21

誤解があります: 継承 - 純粋な OOP の概念の外では、C++ はそうではありません - は、「減衰機能を備えた名前のないメンバーを持つ構成」にすぎません。

仮想関数が存在しない (そしてデストラクタはこの意味で特別ではない) ため、オブジェクトはポリモーフィックではなくなりますが、「動作を再利用してネイティブ インターフェイスを公開する」だけの場合、継承はまさにあなたが要求したことを行います。

デストラクタの呼び出しは常に仕様によって連鎖されているため、デストラクタを相互に明示的に呼び出す必要はありません。

#include <iostream>
unsing namespace std;

class A
{
public:
   A() { cout << "A::A()" << endl; }
   ~A() { cout << "A::~A()" << endl; }
   void hello() { cout << "A::hello()" << endl; }
};

class B: public A
{
public:
   B() { cout << "B::B()" << endl; }
   ~B() { cout << "B::~B()" << endl; }
   void hello() { cout << "B::hello()" << endl; }
};

int main()
{
   B b;
   b.hello();
   return 0;
}

出力します

A::A()
B::B()
B::hello()
B::~B()
A::~A()

で A を B に埋め込む

class B
{
public:
   A a;
   B() { cout << "B::B()" << endl; }
   ~B() { cout << "B::~B()" << endl; }
   void hello() { cout << "B::hello()" << endl; }
};

まったく同じように出力されます。

「デストラクタが仮想でない場合は派生させない」は C++ の必須の結果ではありませんが、C の前に発生する一般的に受け入れられている書かれていない (仕様には何もありません: ベースで削除を呼び出す UB を除いて) ルールです。 ++99、動的継承と仮想関数による OOP が C++ でサポートされる唯一のプログラミング パラダイムであったとき。

もちろん、世界中の多くのプログラマーは、そのような学校でを作ってきました (iostream をプリミティブとして教え、次に配列とポインターに移動し、最後のレッスンで教師は「ああ... tehre もベクトル、文字列、およびその他の高度な機能を備えた STL」)、そして今日、C++ がマルチパラダイムになったとしても、この純粋な OOP ルールを主張し続けています。

私のサンプルでは、​​A::~A() は A::hello とまったく同じ仮想ではありません。どういう意味ですか?

A::hello単純: を呼び出しても が呼び出されないのと同じ理由で、 (削除によって) をB::hello呼び出しても は呼び出されません。あなたのプログラミング スタイルで最初のアサーションを受け入れることができるなら、2 番目のアサーションを受け入れることができない理由はありません。私のサンプルでは、​​ A::~A は virtual ではなく、それが何を意味するかを知っているため、受け取るものはありません。A::~A()B::~B()A* p = new Bdelete p

B の 2 番目の例を で使用すると、まったく同じ理由で が作成されますがA* p = &((new B)->a);delete p;この 2 番目のケースは、最初のケースと完全に二重であり、明確な理由もなく、誰にも興味がないように見えます。

唯一の問題は「メンテナンス」です。つまり、OOP プログラマーが yopur コードを表示した場合、それ自体が間違っているからではなく、そうするように言われたために拒否するという意味です。

実際、「デストラクタが仮想でない場合は派生させない」というのは、ほとんどのプログラマが、 base へのポインタで delete を呼び出せないことを知らないプログラマが多すぎると信じているためです。(これが礼儀正しくない場合は申し訳ありませんが、30年以上のプログラミング経験の後、他に理由が見当たりません!)

しかし、あなたの質問は異なります:

B::~B() を (削除またはスコープ終了によって) 呼び出すと、常に A::~A() になります。これは、A (埋め込みまたは継承のいずれであっても)がいずれにせよ B の一部であるためです。


Luchian のコメントに続いて: 彼のコメントの上記で言及された未定義の動作は、仮想デストラクタのないオブジェクトへのポインタのベースでの削除に関連しています。

OOPスクールによると、これは「仮想デストラクタが存在しない場合は派生しない」というルールになります。

ここで私が指摘しているのは、その学校の理由は、すべての OOP 指向のオブジェクトがポリモーフィックである必要があり、すべてがポリモーフィックであるという事実に依存しているということです。それらの主張を行うことにより、その学校は、純粋な OOP プログラムがその UB を経験しないように、派生したものと置き換え不可能なものの間の交差を意図的に無効にしようとしています。

私の立場は、単純に、C++ は単なる OOP ではなく、すべての C++ オブジェクトがデフォルトで OOP 指向でなければならないわけではないことを認め、OOP が常に必要な必要性ではないことを認め、C++ の継承が常に OOP に対応しているわけではないことも認めています。代用。

std::map はポリモーフィックではないため、置き換え可能ではありません。MyMap も同じです。ポリモーフィックではなく、置き換え可能ではありません。

std::map を再利用して、同じ std::map インターフェイスを公開するだけです。そして、継承は、再利用された関数を呼び出すだけの、書き換えられた関数の長いボイラープレートを回避する方法です。

std::map には仮想 dtor がないため、MyMap には仮想 dtor がありません。これは、私にとっては、これらがポリモーフィック オブジェクトではなく、一方を他方の代わりに使用してはならないことを C++ プログラマーに伝えるのに十分です。

この立場は、今日、ほとんどの C++ 専門家によって共有されていないことを認めなければなりません。しかし、私は(私の唯一の個人的な意見ですが)これは、C++ の必要性のためではなく、提供すべきドグマとしての OOP に関連する彼らの歴史のためだけだと思います。私にとって、C++ は純粋な OOP 言語ではなく、OOP に従わない、または必要とされないコンテキストでは、必ずしも OOP パラダイムに従う必要はありません。

于 2012-05-07T08:21:04.890 に答える
13

継承したいstd::map[...]

なんで ?

継承する伝統的な理由は 2 つあります。

  • そのインターフェースを再利用する (したがって、それに対してコード化されたメソッド)
  • その動作を再利用する

前者にはメソッドがないため、ここでは意味がmapありませんvirtual。したがって、継承によってその動作を変更することはできません。後者は、最終的にメンテナンスを複雑にするだけの継承の使用の倒錯です。


意図した使用法が明確にわからない場合 (質問のコンテキストが不足している場合)、実際に必要なのは、いくつかのボーナス操作を備えたマップのようなコンテナーを提供することだと思います。これを実現するには、次の 2 つの方法があります。

  • コンポジション: を含む新しいオブジェクトを作成しstd::map、適切なインターフェイスを提供します。
  • 拡張: で動作する新しい自由関数を作成します。std::map

後者はよりシンプルですが、よりオープンでもあります。 の元のインターフェースstd::mapは依然として広く開かれています。したがって、操作を制限するのには適していません。

前者は間違いなくより重いですが、より多くの可能性を提供します。

2 つのアプローチのどちらがより適しているかを判断するのは、あなた次第です。

于 2012-05-07T07:20:13.470 に答える