あなたはstd :: vectorから継承しないでください


189

わかりました、これを告白するのは本当に難しいですが、私は現在、から継承する強い誘惑がありstd::vectorます。

ベクター用に約10のカスタマイズされたアルゴリズムが必要であり、それらを直接ベクターのメンバーにしたい。しかし当然ながら、の残りstd::vectorのインターフェイスも必要です。さて、私の最初のアイデアは、法律を遵守する市民std::vectorとして、MyVectorクラスにメンバーを含めることでした。しかし、私はすべてのstd :: vectorのインターフェースを手動で再提供する必要があります。入力するには多すぎます。次に、私はプライベート継承について考えました。そのため、メソッドを再提供する代わりusing std::vector::memberに、パブリックセクションにをまとめて記述します。これも実際には退屈です。

そして、私は実際に、私は単純にからパブリックに継承できると本当に思っていますがstd::vector、このクラスは多態的に使用されるべきではないという警告をドキュメントに提供しています。ほとんどの開発者は、これを多態的に使用してはならないことを理解するのに十分な能力があると思います。

私の決定は完全に不当なものですか?もしそうなら、なぜですか?追加のメンバーが実際にはメンバーになるが、ベクターのすべてのインターフェースを再入力する必要がない代替案を提供できますか?疑わしいですが、できれば幸せです。

また、一部の馬鹿が次のようなものを書くことができるという事実は別として

std::vector<int>* p  = new MyVector

MyVectorを使用する上で他の現実的な危険はありますか?現実的と言うことで、ベクトルへのポインタを受け取る関数を想像するようなものを破棄します...

さて、私は私のケースを述べました。私は罪を犯しました。今私を許すかどうかはあなた次第です:)


9
それで、あなたは基本的に、コンテナのインターフェースを再実装するのが面倒すぎるという事実に基づいて、一般的なルールに違反しても大丈夫かどうか尋ねていますか?では、そうではありません。ほら、あの苦い薬を飲み込んで正しく飲めば、両方の長所を手に入れることができます。あの人になってはいけない。堅牢なコードを記述します。
ジムブリソム

7
非メンバー関数で必要な機能を追加できない/できないのはなぜですか?私にとって、これはこのシナリオで行うのが最も安全な方法です。
Simone、2010

11
@ジム:std::vectorのインターフェイスは非常に巨大で、C ++ 1xが登場すると大幅に拡張されます。これは入力するのが大変で、数年でさらに拡張する必要があります。これは、封じ込めの代わりに継承を検討するのに十分な理由だと思います。これらの関数がメンバーであるという前提に従うと(私はこれを疑います)。STLコンテナーから派生しないルールは、それらが多態性ではないことです。それらをそのように使用していない場合は、適用されません。
sbi

9
問題の真骨頂は一文にあります:「私は彼らを直接ベクトルのメンバーにしてもらいたい」です。問題の他の何も本当に重要ではありません。なぜこれが「欲しい」のですか?非メンバーとしてこの機能を提供するだけの場合の問題は何ですか?
jalf

8
@JoshC: "Thou shalt"は常に "thou should"よりも一般的であり、それはKing James聖書に見られるバージョンでもあります(これは一般的に人々が "thou shalt not [...] ")。いったい何を「ミススペル」と呼べるでしょうか?
ruakh 2014

回答:


155

実際、のパブリック継承に問題はありませんstd::vector。これが必要な場合は、それを実行してください。

それが本当に必要な場合にのみ、そうすることをお勧めします。あなたが自由な機能であなたが望むことを行うことができない場合にのみ(例えば、いくつかの状態を保つべきです)。

問題は、それMyVectorが新しい実体であることです。それは、新しいC ++開発者がそれを使用する前にそれが一体何であるかを知っているべきであることを意味します。違いは何だstd::vectorとはMyVector?あちこちでどちらを使うのが良いですか?に移動std::vectorする必要がある場合はどうなりMyVectorますか?ただ使ってswap()もいいですか?

見栄えを良くするためだけに新しいエンティティを作成しないでください。これらのエンティティ(特に、このような一般的なもの)は、孤立して存在することはありません。彼らは常に増加するエントロピーと混合環境に住んでいます。


7
これに対する私の唯一の反論は、彼がこれを行うために何をしているのかを本当に知る必要があるということです。たとえば、追加のデータメンバーをに導入せず、またはMyVectorを受け入れる関数に渡そうとしないでください。std :: vector *またはstd :: vector&を使用して何らかのコピー割り当てが行われる場合、新しいデータメンバーがコピーされないというスライスの問題があります。同じことは、ベースポインター/参照を通じてスワップを呼び出す場合にも当てはまります。オブジェクトのスライスを危険にさらすあらゆる種類の継承階層は悪いものだと思いがちです。std::vector&std::vector*MyVector
stinky472 2012

13
std::vectorデストラクタではありませんvirtualので、あなたがそれを継承することはありません、
アンドレあるFratelli

2
このため、std :: vectorを公に継承したクラスを作成しました。STL以外のベクタークラスを含む古いコードがあり、STLに移行したいと考えていました。std :: vectorの派生クラスとして古いクラスを再実装し、std :: vectorを使用して新しいコードを記述しながら、古いコードで古い関数名(たとえば、size()ではなくCount())を引き続き使用できるようにしました関数。データメンバーを追加しなかったため、ヒープ上に作成されたオブジェクトに対してstd :: vectorのデストラクタが正常に機能しました。
Graham Asher

3
@GrahamAsherベースへのポインタを介してオブジェクトを削除し、デストラクタが仮想でない場合、プログラムは未定義の動作を示しています。未定義の動作の考えられる結果の1つは、「テストでは問題なく動作した」ことです。もう1つは、祖母にWeb閲覧履歴をメールで送信することです。どちらもC ++標準に準拠しています。コンパイラ、OS、または月の満ち欠けのポイントリリースで、1つから別のバージョンに変更されることも準拠しています。
Yakk-Adam Nevraumont

2
@GrahamAsherいいえ、仮想デストラクタなしでbaseへのポインタを介してオブジェクトを削除するときはいつでも、それは標準では未定義の動作です。何が起こっていると思いますか。あなたはたまたま間違っている。「基本クラスデストラクタが呼び出され、機能する」は、この未定義の動作の1つの考えられる症状(および最も一般的な)です。これは、コンパイラが通常生成する単純なマシンコードであるためです。これは安全でも安全でもありません。
Yakk-Adam Nevraumont

92

STL全体は、アルゴリズムとコンテナが分離するように設計されています

これにより、constイテレータ、ランダムアクセスイテレータなど、さまざまなタイプのイテレータの概念が生まれました。

したがって、私はこの規則を受け入れ、彼らが取り組んでいるコンテナが何であるかを気にしないようにアルゴリズム設計することをお勧めします-そして彼らは彼らが実行する必要がある特定のタイプのイテレータだけを必要とするでしょう操作。

また、Jeff Attwoodによるいくつかの良い発言にリダイレクトさせてください。


63

std::vectorパブリックから継承しない主な理由は、子孫の多態的使用を効果的に防止する仮想デストラクタがないことです。特に、(派生クラスがメンバーを追加しない場合でも)実際に派生オブジェクトを指すは許可さdeletestd::vector<T>*いませんが、コンパイラーは通常、警告を出せません。

これらの条件下ではプライベート継承が許可されます。したがって、以下に示すように、プライベート継承を使用し、親から必要なメソッドを転送することをお勧めします。

class AdVector: private std::vector<double>
{
    typedef double T;
    typedef std::vector<double> vector;
public:
    using vector::push_back;
    using vector::operator[];
    using vector::begin;
    using vector::end;
    AdVector operator*(const AdVector & ) const;
    AdVector operator+(const AdVector & ) const;
    AdVector();
    virtual ~AdVector();
};

まず、アルゴリズムをリファクタリングして、操作対象のコンテナのタイプを抽象化し、回答者の大半が指摘しているように、それらを無料のテンプレート関数として残しておくことを検討する必要があります。これは通常、アルゴリズムがコンテナではなくイテレータのペアを引数として受け入れるようにすることで行われます。


IIUC、仮想デストラクタがないことは、派生クラスが破棄時に解放する必要があるリソースを割り当てる場合にのみ問題になります。(ポリモーフィックなユースケースでは解放されません。ベースへのポインターを介して派生オブジェクトの所有権を無意識に取得するコンテキストは、ベースデストラクターを呼び出すのはその時だけです。)他のオーバーライドされたメンバー関数から同様の問題が発生するため、注意が必要です。基本的なものを呼び出すことが有効であると見なされます。しかし、追加のリソースがない場合、他の理由はありますか?
ピーター-2016年

2
vectorの割り当てられたストレージは問題ではありません-結局のところ、vectorへのポインタを通じてのデストラクタが正しく呼び出されvectorます。これは、標準が基本クラス式を介して無料のストアオブジェクトを使用deleteすることを禁止しているだけです。理由は確かに、(特定のサイズのオブジェクトに複数の割り当て領域がある場合など)割り当てメカニズムが、メモリチャンクのサイズをのオペランドから解放するように推測しようとする可能性があるためです。この制限は、静的または自動の保存期間を持つオブジェクトの通常の破棄には適用されません。delete
ピーター-モニカを復活させる

@DavisHerring私たちはそこに同意すると思います:-)。
ピーター-モニカの復活2017

@DavisHerringああ、そうですね、あなたは私の最初のコメントを参照しています。そのコメントにはIIUCがあり、質問で終わりました。実際、それは常に禁止されていることを後で見ました。(Basilevsは「効果的に防止する」という一般的な声明を出しました、そして私はそれが防止する特定の方法について疑問に思いました。)はい、私たちは同意します:UB。
ピーター-モニカの復活

@Basilevs不注意だったに違いありません。修繕。
ThomasMcLeod 2018

36

あなたがこれを考慮しているなら、あなたは明らかにあなたのオフィスで言語のペダントをすでに殺しました。邪魔にならないように、なぜか

struct MyVector
{
   std::vector<Thingy> v;  // public!
   void func1( ... ) ; // and so on
}

これにより、MyVectorクラスを誤ってアップキャストして発生する可能性のあるすべての失敗を回避し、少し追加するだけですべてのベクトル演算にアクセスできます.v


コンテナとアルゴリズムを公開していますか?上記のコスの答えを見てください。
Bruno nery


19

何を達成したいですか?いくつかの機能を提供するだけですか?

これを行うC ++の慣用的な方法は、機能を実装するいくつかの無料の関数を記述することです。おそらく実装する機能のためにstd :: vectorを実際に必要としない可能性があります。つまり、std :: vectorから継承しようとすることで、再利用性を失うことになります。

標準ライブラリとヘッダーを見て、それらがどのように機能するかを黙想することを強くお勧めします。


5
私は確信していません。その理由を説明するために提案されたコードのいくつかで更新できますか?
Karl Knechtel、2010

6
@Armen:美学以外に、何か正当な理由はありますか?
snemarch 2010

12
@Armen:より優れた美学、およびより一般的なものは、無料frontback機能も提供することです。:)(無料の例beginendC ++ 0x の例も検討してください。)
UncleBens

3
私はまだ無料の機能の何が悪いのかわかりません。STLの「美学」が気に入らない場合は、おそらくC ++は美的に間違った場所です。また、一部のメンバー関数を追加しても修正されません。他の多くのアルゴリズムはまだ無料の関数であるためです。
フランクオスターフェルド

17
重い演算の結果を外部アルゴリズムでキャッシュするのは困難です。ベクトル内のすべての要素の合計を計算するか、ベクトル要素を係数として多項式を解く必要があるとします。それらの操作は重く、遅延はそれらにとって有用でしょう。しかし、コンテナからラップしたり継承したりせずに導入することはできません。
Basilevs

14

盲目的に100%の確率で従うべきルールはほとんどないと思います。あなたはかなり多くの考えを与えたように聞こえ、これが進むべき道であると確信しています。ですから、誰かがこれをしないという明確な理由を考え出さない限り、私はあなたの計画を進めるべきだと思います。


9
あなたの最初の文は常に100%真実です。:)
スティーブファローズ

5
残念ながら、2番目の文は違います。彼はそれについて多くのことを考えていません。質問のほとんどは無関係です。彼の動機を示す唯一の部分は、「彼らを直接ベクターのメンバーにしてもらいたい」です。が欲しいです。ない理由ない、なぜこれが望ましいです。彼はそれをまったく考えてなかったように聞こえます
jalf

7

独自の方法での定義の隠された詳細を処理するため、またはの代わりにそのようなクラスのオブジェクトを使用するイデオロギー上の理由がない限り、とはstd::vector異なる動作をするクラスを作成する必要がない限り、継承する理由はありません。のもの。ただし、C ++の標準の作成者は、そのような継承されたクラスが特定の方法でベクトルを改善するために利用できるインターフェイスを(保護されたメンバーの形で)提供していませんでした。実際、彼らは、拡張を追加したり、追加の実装を微調整したりする必要があるかもしれない特定の側面を考える方法がなかったので、目的のためにそのようなインターフェースを提供することを考える必要はありませんでした。std::vectorstd::vectorstd::vectorstd::vector

std::vectorsは多態性ではないため、2番目のオプションの理由はイデオロギーにすぎない可能性があります。それ以外の場合、std::vectorパブリック継承を介してパブリックインターフェイスを公開するか、パブリックメンバーシップを介して公開するかに差はありません。(フリー関数で逃げられないように、オブジェクトの状態を維持する必要があるとします)。あまり健全ではないメモとイデオロギーの観点から見ると、std::vectorsは一種の「単純なアイデア」であるように思われるため、さまざまなクラスのオブジェクトの形での複雑さは、イデオロギー的には役に立ちません。


すばらしい答えです。SOへようこそ!
Armen Tsirunyan 2014

4

実際的には、派生クラスにデータメンバーがない場合は、多態的な使用であっても問題はありません。仮想デストラクタが必要になるのは、基本クラスと派生クラスのサイズが異なる場合、および/または仮想関数(vテーブルを意味する)がある場合のみです。

理論的には: C ++ 0x FCDの[expr.delete]から:最初の代替(オブジェクトの削除)では、削除されるオブジェクトの静的タイプが動的タイプと異なる場合、静的タイプは削除するオブジェクトの動的タイプの基本クラスと静的タイプには、仮想デストラクタがあるか、動作が定義されていません。

しかし、問題なくstd :: vectorからプライベートに派生させることができます。次のパターンを使用しました。

class PointVector : private std::vector<PointType>
{
    typedef std::vector<PointType> Vector;
    ...
    using Vector::at;
    using Vector::clear;
    using Vector::iterator;
    using Vector::const_iterator;
    using Vector::begin;
    using Vector::end;
    using Vector::cbegin;
    using Vector::cend;
    using Vector::crbegin;
    using Vector::crend;
    using Vector::empty;
    using Vector::size;
    using Vector::reserve;
    using Vector::operator[];
    using Vector::assign;
    using Vector::insert;
    using Vector::erase;
    using Vector::front;
    using Vector::back;
    using Vector::push_back;
    using Vector::pop_back;
    using Vector::resize;
    ...

3
「基本クラスと派生クラスのサイズが異なるか、仮想関数(vテーブルを意味する)がある場合にのみ、仮想デストラクタが必要です。」この主張は実際には正しいですが、理論的には
違います

2
うん、原則としてそれはまだ未定義の動作です。
jalf

これが未定義の動作であると主張する場合は、証明(標準からの引用)を参照してください。
hmuelner、2010

8
@hmuelner:残念ながら、Armenとjalfはこれで正しいです。[expr.delete]そのダイナミック型とは異なるオブジェクトの静的タイプを削除する場合、最初の選択肢(削除対象)において<引用>、静的タイプは、ダイナミックタイプのベース・クラスでなければならない:C ++ 0X FCDで削除されるオブジェクトの静的型には仮想デストラクタが含まれるか、動作が未定義です。</ quote>
Ben Voigt

1
これはおかしいです。なぜなら、動作は重要なデストラクタの存在に依存していると私が実際に考えたからです(具体的には、PODクラスはベースへのポインタによって破棄される可能性があります)。
Ben Voigt

3

良いC ++スタイルに従っている場合、仮想関数が存在しないことが問題ではなく、スライスされますhttps://stackoverflow.com/a/14461532/877329を参照)

仮想関数がないことが問題ではないのはなぜですか?関数はdelete所有権を持たないため、関数が受け取るポインターを試行してはなりません。したがって、厳密な所有権ポリシーに従う場合、仮想デストラクタは必要ありません。たとえば、これは常に間違っています(仮想デストラクタの有無にかかわらず)。

void foo(SomeType* obj)
    {
    if(obj!=nullptr) //The function prototype only makes sense if parameter is optional
        {
        obj->doStuff();
        }
    delete obj;
    }

class SpecialSomeType:public SomeType
    {
    // whatever 
    };

int main()
    {
    SpecialSomeType obj;
    doStuff(&obj); //Will crash here. But caller does not know that
//  ...
    }

対照的に、これは常に機能します(仮想デストラクタの有無にかかわらず)。

void foo(SomeType* obj)
    {
    if(obj!=nullptr) //The function prototype only makes sense if parameter is optional
        {
        obj->doStuff();
        }
    }

class SpecialSomeType:public SomeType
    {
    // whatever 
    };

int main()
    {
    SpecialSomeType obj;
    doStuff(&obj);
//  The correct destructor *will* be called here.
    }

オブジェクトがファクトリーによって作成されたdelete場合、ファクトリーは独自のヒープを使用する可能性があるため、ファクトリーはの代わりに使用する必要のある作業中の削除プログラムへのポインターも返す必要があります。呼び出し元は、a share_ptrまたはの形式で取得できます unique_ptr。つまり、から直接delete取得しなかったものは何もしないでください。new


2

はい、安全でないことをしないように注意している限り、安全です... newでベクターを使用する人を見たことがないので、実際には大丈夫でしょう。ただし、C ++では一般的なイディオムではありません...

アルゴリズムとは何かについて、より多くの情報を提供できますか?

時々、あなたはデザインである道を下って行き、その後あなたが取ったかもしれない他の道を見ることはできません-あなたが私のために10個の新しいアルゴリズムでベクトル化する必要があると主張する事実は私に警報ベルを鳴らします-本当に10個の一般的な目的がありますベクトルが実装できるアルゴリズム、または汎用ベクトルであり、かつアプリケーション固有の機能を含むオブジェクトを作成しようとしていますか?

あなたがこれをしてはいけないと言っているのではありません。警報ベルを鳴らしている情報が鳴っているだけで、抽象化に何か問題があり、あなたが達成するより良い方法があると思います。欲しいです。


2

私もstd::vector最近受け継いだもので、とても重宝しており、今のところ問題はありません。

私のクラスは疎行列クラスですstd::vector。つまり、行列要素をどこかに、つまりに保存する必要があり ます。私が継承する理由は、すべてのメソッドへのインターフェースを書くのが少し面倒だったことと、SWIGを介してPythonにクラスをインターフェースしていることですstd::vector。このインターフェイスコードをクラスに拡張する方が、新しいコードを最初から作成するよりもはるかに簡単です。

私はアプローチと見ることができる唯一の問題はそれほど非仮想デストラクタではなく、むしろのような私が過負荷に希望のいくつかの他の方法は、push_back()resize()insert()などのプライベート継承は確かに良いオプションである可能性があります。

ありがとう!


10
私の経験では、最悪の長期的な被害は、多くの場合、不適切なアドバイスを試みた人々によって引き起こされ、「これまでのところ、問題を経験していません(読んだことに気付きました)」。
幻滅2014

0

ここで、あなたが望むことを行うための2つの方法を紹介しましょう。1つはラップするもう1つの方法でstd::vector、もう1つはユーザーに何も壊す機会を与えずに継承する方法です。

  1. std::vectorたくさんの関数ラッパーを書かずに別のラップ方法を追加しましょう。

#include <utility> // For std:: forward
struct Derived: protected std::vector<T> {
    // Anything...
    using underlying_t = std::vector<T>;

    auto* get_underlying() noexcept
    {
        return static_cast<underlying_t*>(this);
    }
    auto* get_underlying() const noexcept
    {
        return static_cast<underlying_t*>(this);
    }

    template <class Ret, class ...Args>
    auto apply_to_underlying_class(Ret (*underlying_t::member_f)(Args...), Args &&...args)
    {
        return (get_underlying()->*member_f)(std::forward<Args>(args)...);
    }
};
  1. 代わりにstd :: spanから継承しstd::vector、dtorの問題を回避します。

0

この質問は、息をのむようなパールクラッチングを生成することが保証されていますが、実際には、標準コンテナからの派生を回避する、または回避するために「不必要にエンティティを増やす」ための正当な理由はありません。最も単純で可能な限り短い表現が最も明確で、最良です。

派生型については通常の注意を払う必要がありますが、標準のベースの場合について特別なことは何もありません。ベースメンバー関数をオーバーライドするのは難しいかもしれませんが、非仮想ベースを使用するのは賢明ではないため、ここではそれほど特別なことはありません。データメンバーを追加する場合、メンバーがベースのコンテンツと一貫性を保つ必要がある場合、スライスについて心配する必要がありますが、これもどのベースでも同じです。

標準コンテナから派生することが特に有用であることがわかった場所は、必要な初期化を正確に行う単一のコンストラクタを追加することです。他のコンストラクタによる混乱やハイジャックの可能性はありません。(私はあなた、initialization_listコンストラクターを見ています!)その後、スライスされた結果のオブジェクトを自由に使用できます-ベースを期待するものへの参照によって渡し、それからベースのインスタンスに移動します。テンプレート引数を派生クラスにバインドするのが面倒でない限り、心配する必要のある特別なケースはありません。

このテクニックがC ++ 20ですぐに役立つ場所は予約です。私たちが書いたかもしれないところ

  std::vector<T> names; names.reserve(1000);

言うことが出来る

  template<typename C> 
  struct reserve_in : C { 
    reserve_in(std::size_t n) { this->reserve(n); }
  };

クラスのメンバーとしても

  . . .
  reserve_in<std::vector<T>> taken_names{1000};  // 1
  std::vector<T> given_names{reserve_in<std::vector<T>>{1000}}; // 2
  . . .

(好みに応じて)そして、reserve()を呼び出すためだけにコンストラクタを書く必要はありません。

reserve_in技術的にはC ++ 20を待つ必要がある理由は、以前の標準では空のベクトルの容量を移動間で保持する必要がないためです。これは見落としとして認識され、合理的に修正されることが期待できます。 20年までの時間の欠陥として。既存のすべての実装は実際に移動間で容量を保持するため、修正は以前の標準に効果的に戻されることも期待できます。標準はそれを必要としなかっただけです。銃-予約はほとんどの場合、とにかく最適化にすぎません。)

のケースはreserve_in無料の関数テンプレートの方が適していると主張する人もいます。

  template<typename C> 
  auto reserve_in(std::size_t n) { C c; c.reserve(n); return c; }

そのような代替案は確かに実行可能であり、* RVOが原因で、場合によっては非常に高速になることさえあります。ただし、派生または自由関数の選択は、標準コンポーネントからの派生に関する根拠のない(へっ!)迷信からではなく、独自のメリットに基づいて行う必要があります。上記の使用例では、2番目のフォームのみがfree関数で機能します。クラスのコンテキストの外では、少し簡潔に書くことができます:

  auto given_names{reserve_in<std::vector<T>>(1000)}; // 2
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.