私はEffective C++でなされた特定のポイントについて尋ねたかったのです。
それは言う:
クラスがポリモーフィッククラスのように動作する必要がある場合は、デストラクタを仮想化する必要があります。さらに、_
std::string
_には仮想デストラクタがないため、そこから派生することはできません。また、_std::string
_は、基本クラスになるように設計されていません。ポリモーフィック基本クラスを忘れてください。
(多態性ではなく)基本クラスになる資格を得るには、クラスで具体的に何が必要かわかりません。
_std::string
_クラスから派生すべきではない唯一の理由は、仮想デストラクタがないことですか?再利用を目的として、基本クラスを定義し、複数の派生クラスから継承できます。では、_std::string
_が基本クラスとして適格にならない理由は何ですか?
また、再利用を目的として純粋に定義された基本クラスがあり、多くの派生型がある場合、クラスが多態的に使用されることを意図していないため、クライアントがBase* p = new Derived()
を実行できないようにする方法はありますか?
このステートメントは、ここでの混乱を反映していると思います(強調は私のものです)。
私はクラスで基本クラスになる資格を得るのに特に必要なものを理解していません(ポリモーフィックではない)?
慣用的なC++では、クラスから派生するための2つの用途があります。
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);
}
一般的なアドバイスの反対側を提供する(これは、特定の冗長性/生産性の問題が明らかでない場合に適切です)...
仮想デストラクタのないベースからのパブリックデリバリーが適切な決定であるシナリオが少なくとも1つあります。
これはかなり制限的に聞こえるかもしれませんが、このシナリオに一致する実際のプログラムには多くのケースがあります。
プログラミングは妥協についてです。より概念的に「正しい」プログラムを書く前に:
潜在的な問題にオブジェクトの使用法が含まれている場合、そのプログラムのアクセス性、スコープ、および使用法の性質について洞察を与えられた人が想像できないような、または次の場合のコンパイル時エラーを生成できます。危険な使用(たとえば、派生クラスのサイズがベースのサイズと一致するというアサーション。これにより、新しいデータメンバーの追加が妨げられます)、それ以外の場合は、時期尚早のオーバーエンジニアリングになる可能性があります。クリーンで直感的な簡潔なデザインとコードで簡単に勝ち取りましょう。
たとえば、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++コーディング標準の項目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の問題リストに続きます...
非メンバー関数は、すでに
string
sを操作している既存のコード内でうまく機能します。代わりにsuper_string
を指定すると、コードベースを介して強制的に変更が行われ、型と関数のシグネチャがsuper_string
に変更されます。
この主張(および以下のほとんどの主張)の根本的な誤りは、タイプセーフの利点を無視して、少数のタイプのみを使用することの利便性を向上させることです。これは、a)の代替として、c)またはb)に対する洞察ではなく、上記のd)の好みを表しています。プログラミングの技術には、合理的な再利用、パフォーマンス、利便性、および安全性を実現するために、異なるタイプの長所と短所のバランスをとることが含まれます。これについては、以下の段落で詳しく説明しています。
パブリック派生を使用して、既存のコードは暗黙的に基本クラスstring
にstring
としてアクセスし、通常どおりに動作し続けることができます。既存のコードが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_string
がstring
の関数の一部を隠している(そして派生クラスで非仮想関数を再定義してもオーバーライドされない場合、それは単に非表示になっている)場合、その生涯を開始したstring
sを操作するコードで広範な混乱を引き起こす可能性がありますsuper_string
sから自動的に変換されます。
構成にも当てはまります。また、コードがデフォルトで通過したり同期したりしないように設定されているため、発生する可能性が高く、ランタイムポリモーフィック階層がある状況でも当てはまります。最初は交換可能に見えるクラスで異なる動作をするSamed名前付き関数-単に厄介です。これは事実上、正しいOOプログラミングに対する通常の注意であり、型安全などの利点を放棄する十分な理由にはなりません。
super_string
がstring
から継承してさらに追加したい場合state [スライスの説明]
同意します-良い状況ではありません。削除の問題が理論の領域から非常に実用的な領域へとポインタを介して移動することが多いため、私は個人的に線を引く傾向があります。追加のメンバーに対してデストラクタは呼び出されません。それでも、スライスによって、希望どおりの結果が得られることがよくあります。super_string
を派生させて、継承された機能を変更するのではなく、アプリケーション固有の機能の別の「次元」を追加するアプローチを考えると...
確かに、保持したいメンバー関数のパススルー関数を作成するのは面倒ですが、そのような実装は、パブリックまたは非パブリックの継承を使用するよりもはるかに優れており、安全です。
まあ、確かに退屈について同意します...
std::string
から派生する場合、いくつかの関数をインターセプトしてオブジェクトを大文字のままにすることはできません。std::string&
または...*
を介してアクセスするコードは、std::string
の元の関数実装を使用して値を変更できます)そのような導出には問題がないわけではないので、最終結果が手段を正当化しない限り、それを考慮しないでください。とは言っても、特定の場合にこれを安全かつ適切に使用できないという主張は、きっぱりと拒否します。線を引く場所の問題です。
私は時々std::map<>
、std::vector<>
、std::string
などから派生します-私はスライスやbase-via-base-class-pointerの問題に悩まされたことはなく、より重要なことのために多くの時間とエネルギーを節約しました。私はそのようなオブジェクトを異種の多相コンテナに格納しません。ただし、オブジェクトを使用するすべてのプログラマが問題を認識しており、それに応じてプログラミングする可能性があるかどうかを考慮する必要があります。私は個人的に、必要な場合にのみヒープとランタイムのポリモーフィズムを使用するようにコードを書くのが好きですが、一部の人々(Javaバックグラウンド、再コンパイルの依存関係の管理または実行時の動作間の切り替えに対する彼らの好ましいアプローチのため、テスト施設など)を常習的に使用しているため、基本クラスポインターを介した安全な操作についてもっと考慮する必要があります。
デストラクタが仮想ではないだけでなく、std :: stringには仮想関数まったくが含まれておらず、保護されたメンバーもありません。これにより、派生クラスがその機能を変更することが非常に困難になります。
では、なぜそれから派生するのでしょうか?
非ポリモーフィックであることのもう1つの問題は、派生クラスを文字列パラメーターを期待する関数に渡すと、余分な機能が切り捨てられ、オブジェクトがプレーンな文字列として再び表示されることです。
もしあなたが本当にから派生したいのなら(なぜそうしたいのかは議論しないでください)Derived
クラスの直接ヒープのインスタンス化をoperator new
を非公開にすることで防ぐことができると思います:
class StringDerived : public std::string {
//...
private:
static void* operator new(size_t size);
static void operator delete(void *ptr);
};
しかし、このようにして、動的StringDerived
オブジェクトから自分自身を制限します。
なぜc ++ std文字列クラスから派生すべきではないのですか?
必要ありませんだからです。機能拡張にDerivedString
を使用したい場合; _std::string
_の導出に問題はありません。唯一のことは、両方のクラス間でやり取りしてはならないことです(つまり、string
をDerivedString
のレシーバーとして使用しないでください)。
クライアントが
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(); }
//...
};
_
非ポリモーフィッククラスから派生しない理由は2つあります。
std::string
に新しい機能を追加する場合は、まず Boost String Algorithm ライブラリのように、無料の関数(おそらくテンプレート)の使用を検討してください。
新しいデータメンバーを追加する場合は、独自のデザインのクラス内にそれ(埋め込み)を埋め込むことにより、クラスアクセスを適切にラップします。
[〜#〜]編集[〜#〜]:
@Tonyは、私が引用したFunctional理由がおそらくほとんどの人にとって無意味であることを正しく認識しました。優れた設計には、いくつかのソリューションを選択できる場合は、カップリングが弱い方を検討する必要があるという簡単な経験則があります。構成には、継承よりも弱い結合があるため、可能な場合は優先する必要があります。
また、コンポジションはオリジナルのクラスメソッドをうまくラップする機会を与えてくれます。継承(パブリック)を選択し、メソッドが仮想ではない場合(これが当てはまります)、これは不可能です。
派生したstd :: stringクラスにメンバー(変数)を追加するとすぐに、派生したstd :: stringクラスのインスタンスでstd goodiesを使用しようとすると、体系的にスタックをねじ込みますか? stdc ++関数/メンバーは、スタックポインター[インデックス]が(ベースstd :: string)インスタンスサイズのサイズ/境界に固定[および調整]されているためです。
正しい?
私が間違っていたら訂正してください。
C++標準では、Baseクラスデストラクタが仮想ではなく、派生クラスのオブジェクトを指すBaseクラスのオブジェクトを削除すると、未定義の動作が発生することが規定されています。
C++標準セクション5.3.5/3:
オペランドの静的タイプが動的タイプと異なる場合、静的タイプはオペランドの動的タイプの基本クラスであり、静的タイプには仮想デストラクタがあるか、動作が未定義です。
非ポリモーフィッククラスと仮想デストラクタの必要性を明確にするため
デストラクタを仮想化する目的は、delete-expressionによるオブジェクトの多態的な削除を容易にすることです。オブジェクトのポリモーフィックな削除がない場合は、仮想デストラクタは必要ありません。
文字列クラスから派生しない理由
仮想デストラクタがないため、オブジェクトを多態的に削除することができないため、標準のコンテナクラスからの派生は通常避けてください。
文字列クラスについては、文字列クラスには仮想関数がないため、オーバーライドできるものはありません。あなたができる最善は何かを隠すことです。
機能のような文字列が必要な場合は、std :: stringから継承するのではなく、独自のクラスを記述する必要があります。