web-dev-qa-db-ja.com

標準コンテナをサブクラス化/継承しますか?

私はよくこのステートメントをスタックオーバーフローで読みます。個人的には、多態的な方法で使用しているのでない限り、問題はありません。つまり、virtualデストラクタを使用する必要がある場所です。

標準コンテナの機能を拡張/追加したい場合、継承するよりも良い方法は何ですか?これらのコンテナーをカスタムクラス内にラップするには、さらに多くの労力が必要であり、まだクリーンではありません。

49
iammilind

これが悪い考えである理由はいくつかあります。

まず、これは悪い考えです。標準のコンテナには仮想デストラクタがないためです。派生クラスでのクリーンアップを保証できないため、仮想デストラクタを持たないポリモーフィックなものを使用しないでください。

仮想DTORの基本ルール

第二に、それは本当に悪いデザインです。そして、実際にはそれが悪いデザインであるいくつかの理由があります。まず、一般的に動作するアルゴリズムを使用して、常に標準コンテナの機能を拡張する必要があります。これは単純な複雑さの理由です。適用するすべてのコンテナに対してアルゴリズムを記述する必要があり、M個のコンテナとN個のアルゴリズムがある場合、つまりM x Nメソッドを記述する必要があります。アルゴリズムを総称的に記述する場合、N個のアルゴリズムしかありません。したがって、はるかに再利用できます。

また、コンテナーから継承することで適切なカプセル化を解除しているので、これも本当に悪い設計です。経験則としては、型のパブリックインターフェースを使用して必要なことを実行できる場合は、その新しい動作を型の外部で行います。これにより、カプセル化が向上します。実装したい新しい動作の場合は、(アルゴリズムのような)名前空間スコープ関数にします。課す新しい不変条件がある場合は、クラスで包含を使用します。

カプセル化の古典的な説明

最後に、一般に、クラスの動作を拡張する手段として継承を考えるべきではありません。これは、初期のOOP理論の大きな、悪い嘘の1つであり、再利用についての不明確な考え方が原因で発生し、引き続き教えられています悪い理由は明らかですが、今日に昇格しました。継承を使用して動作を拡張すると、ユーザーの手を将来の変更に結び付ける方法で、その拡張動作をインターフェイスコントラクトに結び付けます。たとえば、 TCPプロトコルを使用して通信するタイプSocketのクラスがあり、SocketからクラスSSLSocketを派生させ、Socketの上に上位のSSLスタックプロトコルの動作を実装することにより、その動作を拡張します。今すぐ、同じ通信プロトコルを使用するという新しい要件があるが、USB回線またはテレフォニーを使用しているとします。すべての作業をUSBクラスから派生した新しいクラスまたはテレフォニーにカットアンドペーストする必要があります。そして今、あなたがバグを発見した場合、あなたは3つの場所すべてでそれを修正しなければなりません。時間がかかり、常に修正されるとは限りません...

これは、すべての継承階層に一般的ですA-> B-> C-> ... B、Cなどの派生クラスで拡張した動作を使用したい場合は、基本クラスAのオブジェクトではなく、再設計する必要があるか、実装を複製しています。これにより、将来変更するのが非常に困難な非常にモノリシックな設計につながります(MicrosoftのMFCまたはその.NETを考えてください。あるいは、この間違いをよく犯しています)。代わりに、可能な限り、ほとんどの場合、構成による拡張について考える必要があります。 「オープン/クローズドプリンシプル」を考えている場合は、継承を使用する必要があります。継承されたクラスを通じて抽象基本クラスと動的ポリモーフィズムランタイムが必要です。それぞれが完全な実装になります。階層は深くあるべきではありません-ほとんど常に2つのレベル。タイプセーフティのためにその区別が必要なさまざまな関数に移動するさまざまな動的カテゴリがある場合にのみ、2つ以上を使用してください。これらの場合、実装を持つリーフクラスまで抽象ベースを使用します。

82
ex0du5

ここの多くの人々はこの答えを気に入らないかもしれませんが、異端が言われるべき時であり、そうです...「王は裸です!」

派生に対するすべての動機は弱いです。派生は構成と同じです。これは、「物事をまとめる」ための単なる方法です。コンポジションは名前を付けて物事をまとめ、継承は明示的な名前を付けずに行います。

Std :: vectのインターフェースと実装が同じで、さらに何かを備えたベクトルが必要な場合は、次のことができます。

構成を使用し、それらを委任する関数を実装するすべての埋め込みオブジェクト関数プロトタイプを書き直します(それらが10000の場合...はい:それらすべての10000を書き直す準備をしてください)または...

それを継承し、必要なものだけを追加します(そして、C++の弁護士が継承可能にすることを決定するまで、コンストラクターを書き直します。10年前、「なぜ俳優同士が相互に呼び出せないのか」と、なぜそれがなぜそうであるかについての熱心な議論を覚えています。これは「悪い悪い悪いこと」です... C++ 11がそれを許可し、突然すべての狂信者が黙ってしまうまでです!)、新しいデストラクタは元のデストラクタであったため、仮想ではありません。

some仮想メソッドとsomeを持たないすべてのクラスと同様に、ベースをアドレス指定することによって派生した非仮想メソッドを呼び出すふりをすることはできません。同じことが当てはまります。削除。削除が特定の特別な注意を装うだけの理由はありません。仮想ではないものは呼び出し可能なベースではないことを知っているプログラマーは、派生を割り当てた後にベースで削除を使用しないことも知っています。

「これを回避する」「それをしない」のすべては、常にネイティブに不可知な何かの「道徳化」として聞こえます。言語のすべての機能は、いくつかの問題を解決するために存在しています。問題を解決するための特定の方法が良いか悪いかは、機能自体ではなくコンテキストに依存します。あなたがやっていることが多くのコンテナーを提供する必要がある場合、継承はおそらくではありません(すべてをやり直す必要があります)。それが特定のケースの場合...継承は構成する方法です。忘れるOOP純粋主義:C++は「純粋なOOP」ではなく、コンテナもOOPではありません。

26

標準的なcontianerから公的に派生することは控えてください。 private inheritancecompositionそして、すべての一般的なガイドラインは、関数をオーバーライドしないため、ここでは構成が優れていることを示しているようです。 STLコンテナを公に派生させないでください-本当に必要はありません。

ちなみに、コンテナに一連のアルゴリズムを追加する場合は、イテレータの範囲をとる自立関数として追加することを検討してください。

7
Armen Tsirunyan

パブリック継承は、他の人が述べているすべての理由で問題です。つまり、仮想デストラクタまたは仮想代入演算子を持たない基本クラスにコンテナをアップキャストできるため、 スライスの問題 につながる可能性があります。

一方、個人的に継承することはそれほど問題ではありません。次の例について考えてみます。

#include <vector>
#include <iostream>

// private inheritance, nobody else knows about the inheritance, so nobody is upcasting my
// container to a std::vector
template <class T> class MyVector : private std::vector<T>
{
private:
    // in case I changed to boost or something later, I don't have to update everything below
    typedef std::vector<T> base_vector;

public:
    typedef typename base_vector::size_type       size_type;
    typedef typename base_vector::iterator        iterator;
    typedef typename base_vector::const_iterator  const_iterator;

    using base_vector::operator[];

    using base_vector::begin;
    using base_vector::clear;
    using base_vector::end;
    using base_vector::erase;
    using base_vector::Push_back;
    using base_vector::reserve;
    using base_vector::resize;
    using base_vector::size;

    // custom extension
    void reverse()
    {
        std::reverse(this->begin(), this->end());
    }
    void print_to_console()
    {
        for (auto it = this->begin(); it != this->end(); ++it)
        {
            std::cout << *it << '\n';
        }
    }
};


int main(int argc, char** argv)
{
    MyVector<int> intArray;
    intArray.resize(10);
    for (int i = 0; i < 10; ++i)
    {
        intArray[i] = i + 1;
    }
    intArray.print_to_console();
    intArray.reverse();
    intArray.print_to_console();

    for (auto it = intArray.begin(); it != intArray.end();)
    {
        it = intArray.erase(it);
    }
    intArray.print_to_console();

    return 0;
}

出力:

1
2
3
4
5
6
7
8
9
10
10
9
8
7
6
5
4
3
2
1

クリーンでシンプルで、多くの手間をかけずにstdコンテナを拡張できる自由があります。

そして、あなたがこのような愚かなことをすることを考えているなら:

std::vector<int>* stdVector = &intArray;

あなたはこれを手に入れます:

error C2243: 'type cast': conversion from 'MyVector<int> *' to 'std::vector<T,std::allocator<_Ty>> *' exists, but is inaccessible
5
ErsatzStoat

問題は、あなたや他の誰かが、拡張クラスを誤って基本クラスへの参照を期待している関数に渡す可能性があることです。これは効果的に(そして黙って!)拡張機能を切り離し、見つけにくいバグをいくつか作成します。

いくつかの転送関数を記述しなければならないことは、比較して支払うのに少額の費用のように思えます。

4
Bo Persson

コンテナーから継承する最も一般的な理由は、クラスにメンバー関数を追加するためです。 stdlib自体は変更できないため、継承が代用と考えられています。ただし、これは機能しません。パラメータとしてベクトルを取るフリー関数を実行することをお勧めします。

void f(std::vector<int> &v) { ... }
2
tp1

なぜなら、それらを多態的な方法で使用していないことを保証することはできないからです。あなたは問題を懇願しています。いくつかの関数を書くための努力を払うことは大したことではなく、まして、これを実行したいとさえさえ、よくても疑わしいです。カプセル化はどうなりましたか?

2
Puppy