web-dev-qa-db-ja.com

なぜc ++ std文字列クラスから派生すべきではないのですか?

私はEffective C++でなされた特定のポイントについて尋ねたかったのです。

それは言う:

クラスがポリモーフィッククラスのように動作する必要がある場合は、デストラクタを仮想化する必要があります。さらに、_std::string_には仮想デストラクタがないため、そこから派生することはできません。また、_std::string_は、基本クラスになるように設計されていません。ポリモーフィック基本クラスを忘れてください。

(多態性ではなく)基本クラスになる資格を得るには、クラスで具体的に何が必要かわかりません。

_std::string_クラスから派生すべきではない唯一の理由は、仮想デストラクタがないことですか?再利用を目的として、基本クラスを定義し、複数の派生クラスから継承できます。では、_std::string_が基本クラスとして適格にならない理由は何ですか?

また、再利用を目的として純粋に定義された基本クラスがあり、多くの派生型がある場合、クラスが多態的に使用されることを意図していないため、クライアントがBase* p = new Derived()を実行できないようにする方法はありますか?

64

このステートメントは、ここでの混乱を反映していると思います(強調は私のものです)。

私はクラスで基本クラスになる資格を得るのに特に必要なものを理解していません(ポリモーフィックではない)?

慣用的なC++では、クラスから派生するための2つの用途があります。

  • private継承、テンプレートを使用したミックスインおよびアスペクト指向プログラミングに使用されます。
  • public継承、ポリモーフィックな状況でのみ使用されます[〜#〜] edit [〜#〜]:わかりました。これは、boost::iterator_facadeなどのいくつかのミックスインシナリオでも使用できると思います。 - [〜#〜] crtp [〜#〜] が使用されているときに表示されます。

ポリモーフィックなことをしようとしないのであれば、C++でクラスを公に派生する理由はまったくありません。言語には、言語の標準機能として無料関数が付属しており、ここでは無料関数を使用する必要があります。

このように考えてください-いくつかのメソッドを追加したいだけの理由で、コードのクライアントに独自の文字列クラスの使用に強制的に変換させたいですか? JavaまたはC#(または最も類似したオブジェクト指向言語))とは異なり、C++でクラスを派生させる場合、基本クラスのほとんどのユーザーはその種の変更について知る必要があります。Java/ C#では、クラスは通常C++のポインターと同様の参照を通じてアクセスされます。したがって、クラスのクライアントを分離する間接レベルがあり、他のクライアントに気付かれずに派生クラスを置き換えることができます。

ただし、C++では、クラスはvalue typesです-他のほとんどのOO言語とは異なります。最も簡単な方法は、これは スライスの問題 として知られているものです。

int StringToNumber(std::string copyMeByValue)
{
    std::istringstream converter(copyMeByValue);
    int result;
    if (converter >> result)
    {
        return result;
    }
    throw std::logic_error("That is not a number.");
}

独自の文字列をこのメソッドに渡すと、std::stringのコピーコンストラクターが呼び出され、コピーが作成されます派生オブジェクトのコピーコンストラクターではありません- -std::stringのどの子クラスが渡されても関係ありません。これにより、メソッドと文字列にアタッチされているすべてのメソッドとの間に不整合が生じる可能性があります。関数StringToNumberは、単に派生オブジェクトが何であれそれをコピーすることはできません。単に、派生オブジェクトのサイズがstd::stringとは異なる可能性があるためです-しかし、この関数は、自動でstd::stringのスペースのみを予約するようにコンパイルされましたストレージ。 JavaおよびC#では、関連する自動ストレージのような唯一のものは参照型であり、参照は常に同じサイズであるため、これは問題ではありません。C++ではそうではありません。

簡単に言えば、C++のメソッドを追加するために継承を使用しないでください。これは慣用的ではなく、言語に問題が発生します。可能であれば、非友人、非メンバーの関数を使用してから、構成を行います。テンプレートのメタプログラミングを行っている場合、またはポリモーフィックな動作が必要でない限り、継承を使用しないでください。詳細については、Scott Meyersの Effective C++ 項目23:メンバー関数よりも非メンバー非フレンド関数を優先するを参照してください。

編集:これは、スライスの問題を示すより完全な例です。出力は codepad.org で確認できます

#include <ostream>
#include <iomanip>

struct Base
{
    int aMemberForASize;
    Base() { std::cout << "Constructing a base." << std::endl; }
    Base(const Base&) { std::cout << "Copying a base." << std::endl; }
    ~Base() { std::cout << "Destroying a base." << std::endl; }
};

struct Derived : public Base
{
    int aMemberThatMakesMeBiggerThanBase;
    Derived() { std::cout << "Constructing a derived." << std::endl; }
    Derived(const Derived&) : Base() { std::cout << "Copying a derived." << std::endl; }
    ~Derived() { std::cout << "Destroying a derived." << std::endl; }
};

int SomeThirdPartyMethod(Base /* SomeBase */)
{
    return 42;
}

int main()
{
    Derived derivedObject;
    {
        //Scope to show the copy behavior of copying a derived.
        Derived aCopy(derivedObject);
    }
    SomeThirdPartyMethod(derivedObject);
}
55
Billy ONeal

一般的なアドバイスの反対側を提供する(これは、特定の冗長性/生産性の問題が明らかでない場合に適切です)...

合理的な使用のためのシナリオ

仮想デストラクタのないベースからのパブリックデリバリーが適切な決定であるシナリオが少なくとも1つあります。

  • 専用のユーザー定義型(クラス)によって提供される型の安全性とコードの可読性の利点が必要な場合
  • 既存のベースはデータの保存に理想的であり、クライアントコードでも使用したい低レベルの操作を可能にします
  • その基本クラスをサポートする関数を再利用するのに便利です
  • データが論理的に必要とする追加の不変条件は、派生型としてデータに明示的にアクセスするコードでのみ適用でき、それが設計で「自然に」発生する程度、およびクライアントをどの程度信頼できるかに依存することを理解している論理的に理想的な不変式を理解して連携するためのコード。派生クラスのメンバー関数に期待値を再確認させたい場合があります(そしてスローなど)
  • 派生クラスは、カスタム検索、データのフィルタリング/変更、ストリーミング、統計分析、(代替)イテレーターなど、データに対して機能するタイプ固有の便利な関数をいくつか追加します
  • クライアントコードのベースへの結合は、派生クラスへの結合よりも適切です(ベースが安定しているか、ベースへの変更が派生クラスのコアとなる機能の改善を反映しているため)
    • 別の言い方をすると、クライアントコードが強制的に変更されることを意味する場合でも、派生クラスが基本クラスと同じAPIを公開し続け、ベースAPIと派生APIを拡張できるように絶縁するのではなく、同期の
  • ベースと派生オブジェクトへのポインターを、それらを削除するコードの一部に混在させることはありません。

これはかなり制限的に聞こえるかもしれませんが、このシナリオに一致する実際のプログラムには多くのケースがあります。

背景説明:相対的なメリット

プログラミングは妥協についてです。より概念的に「正しい」プログラムを書く前に:

  • 実際のプログラムロジックを難読化する複雑さとコードを追加する必要があるかどうかを検討します。そのため、特定の問題をより堅牢に処理しても、全体的にエラーが発生しやすくなります。
  • 問題の確率と結果に対して実際的なコストを比較検討し、
  • 「投資収益率」と、あなたが時間を使って他に何ができるかを考えてください。

潜在的な問題にオブジェクトの使用法が含まれている場合、そのプログラムのアクセス性、スコープ、および使用法の性質について洞察を与えられた人が想像できないような、または次の場合のコンパイル時エラーを生成できます。危険な使用(たとえば、派生クラスのサイズがベースのサイズと一致するというアサーション。これにより、新しいデータメンバーの追加が妨げられます)、それ以外の場合は、時期尚早のオーバーエンジニアリングになる可能性があります。クリーンで直感的な簡潔なデザインとコードで簡単に勝ち取りましょう。

仮想デストラクタではなく派生を検討する理由

たとえば、Bから公に派生したクラスDがあるとします。努力することなく、Bでの操作はDで可能です(構築を除きますが、コンストラクターが多数ある場合でも、1つのテンプレートを持つことで効果的な転送を提供できることがよくあります。コンストラクター引数のそれぞれ異なる数:例:template <typename T1, typename T2> D(const T1& x1, const T2& t2) : B(t1, t2) { }。C++ 0x可変テンプレートのより一般化されたソリューション。)

さらに、Bが変更されると、デフォルトでDがそれらの変更を公開し、同期を保ちますが、誰かがDで導入された拡張機能をレビューして、それが有効かどうかを確認する必要がある場合がありますandクライアントの使用法。

これを言い換えると、基本クラスと派生クラス間の明示的な結合が減少しますが、基本クラスとクライアント間の結合が増加しますがあります。

これは多くの場合望んでいることではありませんが、理想的な場合もあれば、問題がない場合もあります(次の段落を参照)。ベースを変更すると、コードベース全体に分散した場所でクライアントコードがさらに変更され、ベースを変更する人がクライアントコードにアクセスして、それに応じてレビューまたは更新することができない場合もあります。ただし、派生クラスプロバイダーとして「中間者」-基本クラスの変更をクライアントにフィードスルーしたい場合、通常はクライアントに-ときどき強制的に-常に関与する必要なく基本クラスが変更される場合は、パブリック派生が理想的です。これは、クラスがそれ自体ではそれほど独立したエンティティではなく、ベースへの薄い付加価値である場合に一般的です。

また、基本クラスのインターフェースが非常に安定しているため、カップリングを問題にしない場合もあります。これは、標準コンテナのようなクラスに特に当てはまります。

要約すると、パブリック派生は、派生クラスの理想的で使い慣れた基本クラスインターフェイスを、メンテナーとクライアントコーダーの両方にとって簡潔かつ自明の方法ですばやく取得または概算するための迅速な方法です-追加機能をメンバー関数として利用できます(どのIMHO(Sutter、Alexandrescuなどと明らかに異なる)は、使いやすさ、読みやすさを助け、IDEを含む生産性向上ツールを支援します)

C++コーディング標準-Sutter&Alexandrescu-短所

C++コーディング標準の項目35は、std::stringから派生するシナリオの問題をリストしています。シナリオが進むにつれ、それは大きくて便利なAPIを公開する負担を示しているのは良いことですが、ベースAPIは非常に安定しているため、標準ライブラリの一部であるため、良い点と悪い点の両方があります。安定したベースは一般的な状況ですが、揮発性のものより一般的ではなく、優れた分析は両方のケースに関連しているはずです。この本の問題のリストを検討しながら、問題の適用可能性を次のような場合と具体的に対比します。

a)class Issue_Id : public std::string { ...handy stuff... }; <-public derivation、私たちの物議を醸す使用法
b)class Issue_Id : public string_with_virtual_destructor { ...handy stuff... }; <-より安全OO派生
c)class Issue_Id { public: ...handy stuff... private: std::string id_; }; <-構成的アプローチ
d)どこでもstd::stringを使用し、独立したサポート関数を使用する

(うまくいけば、コンポジションが許容可能なプラクティスであることに同意できます。これは、カプセル化、型の安全性、およびstd::stringのAPIに加えて、潜在的に強化されたAPIを提供するためです。)

新しいコードを書いていて、OOの意味で概念的なエンティティについて考え始めます。おそらくバグ追跡システム(私はJIRAを考えています)の場合)の1つは、 Issue_Idと言います。データコンテンツはテキスト形式です-アルファベット順のプロジェクトID、ハイフン、増加する問題番号で構成されます:例: "MYAPP-1234"。問題IDはstd::stringに保存でき、手間のかかる小さなテキストがたくさんあります課題IDに必要な検索と操作操作-すでにstd::stringで提供されているものの大きなサブセットと適切な対策のためのいくつか(たとえば、プロジェクトIDコンポーネントの取得、次の可能な課題ID(MYAPP-1235)の提供)。

SutterとAlexandrescuの問題リストに続きます...

非メンバー関数は、すでにstringsを操作している既存のコード内でうまく機能します。代わりにsuper_stringを指定すると、コードベースを介して強制的に変更が行われ、型と関数のシグネチャがsuper_stringに変更されます。

この主張(および以下のほとんどの主張)の根本的な誤りは、タイプセーフの利点を無視して、少数のタイプのみを使用することの利便性を向上させることです。これは、a)の代替として、c)またはb)に対する洞察ではなく、上記のd)の好みを表しています。プログラミングの技術には、合理的な再利用、パフォーマンス、利便性、および安全性を実現するために、異なるタイプの長所と短所のバランスをとることが含まれます。これについては、以下の段落で詳しく説明しています。

パブリック派生を使用して、既存のコードは暗黙的に基本クラスstringstringとしてアクセスし、通常どおりに動作し続けることができます。既存のコードがsuper_string(この場合はIssue_Id)の追加機能を使用したいと考える特別な理由はありません...実際、これは、通常、super_stringを作成しているアプリケーションの既存の低レベルサポートコードです。 、したがって拡張機能によって提供されるニーズに気づかない。たとえば、非メンバー関数to_upper(std::string&, std::string::size_type from, std::string::size_type to)があるとします-それはIssue_Idにも適用できます。

したがって、非メンバーサポート関数がクリーンアップされたり、新しいコードに密結合したりする意図的なコストで拡張されていない限り、変更する必要はありません。 is問題IDをサポートするためにオーバーホールされている場合(たとえば、データコンテンツ形式への洞察を使用して、先頭の英字のみを大文字に変換する場合)、それが実際に行われていることを確認することはおそらく良いことですオーバーロードala to_upper(Issue_Id&)を作成し、型の安全性を可能にする派生または構成のアプローチに固執することにより、Issue_Idを渡しました。 super_stringとコンポジションのどちらを使用しても、労力や保守性に違いはありません。 to_upper_leading_alpha_only(std::string&)の再利用可能な独立型サポート関数はあまり役に立たない可能性があります-そのような関数が最後に欲しかったときを思い出せません。

すべての場所でstd::stringを使用する衝動は、すべての引数をバリアントまたはvoid*sのコンテナーとして受け入れることと質的に異なるわけではないため、任意のデータを受け入れるようにインターフェイスを変更する必要はありませんが、エラーが発生しやすく、自己文書化が少なくなります。コンパイラで検証可能なコード。

文字列を受け取るインターフェース関数は、次のことを行う必要があります。a)super_stringの追加機能を使用しない(役に立たない)。 b)引数をsuper_string(無駄)にコピーします。またはc)文字列参照をsuper_string参照にキャストします(扱いにくく、潜在的に不正)。

これは最初の点を再検討しているようです-今回はサポートコードではなくクライアントコードですが、新しい機能を使用するためにリファクタリングする必要がある古いコード。関数が引数をエンティティとして扱い始めたい場合は、新しい操作が関連している場合、関数はすべき引数をその型として取り始め、クライアントはそれらを生成する必要がありますそのタイプを使用して受け入れます。まったく同じ問題が作曲に存在します。それ以外の場合、醜いものの、以下にリストするガイドラインに従っている場合、c)は実用的で安全です。

文字列にはおそらく保護されたメンバーがないため、super_stringのメンバー関数は、非メンバー関数よりも文字列の内部にアクセスできません(最初から派生することを意図していないことに注意してください)。

確かに、しかしそれは時には良いことです。多くの基本クラスには保護されたデータがありません。パブリックstringインターフェースは、コンテンツを操作するために必要なすべてのものであり、有用な機能(たとえば、上記で想定されているget_project_id())は、これらの操作に関してエレガントに表現できます。概念的には、多くの場合、標準コンテナから派生しましたが、既存のラインに沿ってそれらの機能を拡張またはカスタマイズしたくありませんでした-それらはすでに「完全な」コンテナです-むしろ、特定の動作の別の次元を追加したかった私のアプリケーションに、そしてプライベートアクセスを必要としません。それは、それらがすでに優れたコンテナーであるため、再利用に適しているからです。

super_stringstringの関数の一部を隠している(そして派生クラスで非仮想関数を再定義してもオーバーライドされない場合、それは単に非表示になっている)場合、その生涯を開始したstringsを操作するコードで広範な混乱を引き起こす可能性がありますsuper_stringsから自動的に変換されます。

構成にも当てはまります。また、コードがデフォルトで通過したり同期したりしないように設定されているため、発生する可能性が高く、ランタイムポリモーフィック階層がある状況でも当てはまります。最初は交換可能に見えるクラスで異なる動作をするSamed名前付き関数-単に厄介です。これは事実上、正しいOOプログラミングに対する通常の注意であり、型安全などの利点を放棄する十分な理由にはなりません。

super_stringstringから継承してさらに追加したい場合state [スライスの説明]

同意します-良い状況ではありません。削除の問題が理論の領域から非常に実用的な領域へとポインタを介して移動することが多いため、私は個人的に線を引く傾向があります。追加のメンバーに対してデストラクタは呼び出されません。それでも、スライスによって、希望どおりの結果が得られることがよくあります。super_stringを派生させて、継承された機能を変更するのではなく、アプリケーション固有の機能の別の「次元」を追加するアプローチを考えると...

確かに、保持したいメンバー関数のパススルー関数を作成するのは面倒ですが、そのような実装は、パブリックまたは非パブリックの継承を使用するよりもはるかに優れており、安全です。

まあ、確かに退屈について同意します...

仮想デストラクタではなく成功した派生のガイドライン

  • 理想的には、派生クラスにデータメンバーを追加しないでください。スライスのバリアントは、誤ってデータメンバーを削除し、データメンバーを破壊し、初期化に失敗する可能性があります...
  • さらにそう-非PODデータメンバーを回避する:基本クラスポインターを介した削除は技術的に未定義の動作ですが、デストラクタの実行に失敗した非PODタイプでは、リソースリーク、参照カウントの不良など、非理論的な問題が発生する可能性が高くなります等.
  • リスコフ代用主体を尊重する/新しい不変量を確実に維持できない
    • たとえば、std::stringから派生する場合、いくつかの関数をインターセプトしてオブジェクトを大文字のままにすることはできません。std::string&または...*を介してアクセスするコードは、std::stringの元の関数実装を使用して値を変更できます)
    • アプリケーションでより高いレベルのエンティティをモデル化し、継承された機能を、ベースを使用するが競合しない機能で拡張します。基本タイプによって付与される基本操作(およびそれらの操作へのアクセス)を予期または変更しようとしないでください。
  • カップリングに注意してください。ベースクラスが進化して不適切な機能を備えていても、クライアントクラスに影響を与えずにベースクラスを削除することはできません。
    • 場合によっては、コンポジションを使用する場合でも、パフォーマンス、スレッドセーフティの問題、または値のセマンティクスの欠如のためにデータメンバーを公開する必要があります。そのため、パブリックデリバティブからのカプセル化の喪失が明白に悪化することはありません。
  • 潜在的に派生したクラスを使用している人々がその実装の妥協に気づかない可能性が高いほど、それらを危険にする余裕がありません
    • したがって、多くのアドホックカジュアルユーザーがいる低レベルの広く展開されたライブラリは、アプリケーションレベルや「プライベート」実装/ライブラリで機能を日常的に使用するプログラマによるローカライズされた使用よりも危険な派生に注意する必要があります

概要

そのような導出には問題がないわけではないので、最終結果が手段を正当化しない限り、それを考慮しないでください。とは言っても、特定の場合にこれを安全かつ適切に使用できないという主張は、きっぱりと拒否します。線を引く場所の問題です。

個人的体験

私は時々std::map<>std::vector<>std::stringなどから派生します-私はスライスやbase-via-base-class-pointerの問題に悩まされたことはなく、より重要なことのために多くの時間とエネルギーを節約しました。私はそのようなオブジェクトを異種の多相コンテナに格納しません。ただし、オブジェクトを使用するすべてのプログラマが問題を認識しており、それに応じてプログラミングする可能性があるかどうかを考慮する必要があります。私は個人的に、必要な場合にのみヒープとランタイムのポリモーフィズムを使用するようにコードを書くのが好きですが、一部の人々(Javaバックグラウンド、再コンパイルの依存関係の管理または実行時の動作間の切り替えに対する彼らの好ましいアプローチのため、テスト施設など)を常習的に使用しているため、基本クラスポインターを介した安全な操作についてもっと考慮する必要があります。

24
Tony Delroy

デストラクタが仮想ではないだけでなく、std :: stringには仮想関数まったくが含まれておらず、保護されたメンバーもありません。これにより、派生クラスがその機能を変更することが非常に困難になります。

では、なぜそれから派生するのでしょうか?

非ポリモーフィックであることのもう1つの問題は、派生クラスを文字列パラメーターを期待する関数に渡すと、余分な機能が切り捨てられ、オブジェクトがプレーンな文字列として再び表示されることです。

10
Bo Persson

もしあなたが本当にから派生したいのなら(なぜそうしたいのかは議論しないでください)Derivedクラスの直接ヒープのインスタンス化をoperator newを非公開にすることで防ぐことができると思います:

class StringDerived : public std::string {
//...
private:
  static void* operator new(size_t size);
  static void operator delete(void *ptr);
}; 

しかし、このようにして、動的StringDerivedオブジェクトから自分自身を制限します。

9
beduin

なぜc ++ std文字列クラスから派生すべきではないのですか?

必要ありませんだからです。機能拡張にDerivedStringを使用したい場合; _std::string_の導出に問題はありません。唯一のことは、両方のクラス間でやり取りしてはならないことです(つまり、stringDerivedStringのレシーバーとして使用しないでください)。

クライアントがBase* p = new Derived()を実行しないようにする方法はありますか

はいinlineクラス内のBaseメソッドの周囲にDerivedラッパーを必ず提供してください。例えば.

_class Derived : protected Base { // 'protected' to avoid Base* p = new Derived
  const char* c_str () const { return Base::c_str(); }
//...
};
_
4
iammilind

非ポリモーフィッククラスから派生しない理由は2つあります。

  • Technical:スライシングバグが導入されます(C++では、特に指定のない限り、値で渡されるため)。
  • Functional:ポリモーフィックでない場合、構成といくつかの関数転送で同じ効果を得ることができます

std::stringに新しい機能を追加する場合は、まず Boost String Algorithm ライブラリのように、無料の関数(おそらくテンプレート)の使用を検討してください。

新しいデータメンバーを追加する場合は、独自のデザインのクラス内にそれ(埋め込み)を埋め込むことにより、クラスアクセスを適切にラップします。

[〜#〜]編集[〜#〜]

@Tonyは、私が引用したFunctional理由がおそらくほとんどの人にとって無意味であることを正しく認識しました。優れた設計には、いくつかのソリューションを選択できる場合は、カップリングが弱い方を検討する必要があるという簡単な経験則があります。構成には、継承よりも弱い結合があるため、可能な場合は優先する必要があります。

また、コンポジションはオリジナルのクラスメソッドをうまくラップする機会を与えてくれます。継承(パブリック)を選択し、メソッドが仮想ではない場合(これが当てはまります)、これは不可能です。

2
Matthieu M.

派生したstd :: stringクラスにメンバー(変数)を追加するとすぐに、派生したstd :: stringクラスのインスタンスでstd goodiesを使用しようとすると、体系的にスタックをねじ込みますか? stdc ++関数/メンバーは、スタックポインター[インデックス]が(ベースstd :: string)インスタンスサイズのサイズ/境界に固定[および調整]されているためです。

正しい?

私が間違っていたら訂正してください。

0
Bretzelus

C++標準では、Baseクラスデストラクタが仮想ではなく、派生クラスのオブジェクトを指すBaseクラスのオブジェクトを削除すると、未定義の動作が発生することが規定されています。

C++標準セクション5.3.5/3:

オペランドの静的タイプが動的タイプと異なる場合、静的タイプはオペランドの動的タイプの基本クラスであり、静的タイプには仮想デストラクタがあるか、動作が未定義です。

非ポリモーフィッククラスと仮想デストラクタの必要性を明確にするため
デストラクタを仮想化する目的は、delete-expressionによるオブジェクトの多態的な削除を容易にすることです。オブジェクトのポリモーフィックな削除がない場合は、仮想デストラクタは必要ありません。

文字列クラスから派生しない理由
仮想デストラクタがないため、オブジェクトを多態的に削除することができないため、標準のコンテナクラスからの派生は通常避けてください。
文字列クラスについては、文字列クラスには仮想関数がないため、オーバーライドできるものはありません。あなたができる最善は何かを隠すことです。

機能のような文字列が必要な場合は、std :: stringから継承するのではなく、独自のクラスを記述する必要があります。

0
Alok Save