Stroustrup氏は、「すべてのクラス(Objectクラス)に一意のベースをすぐに発明しないでください。通常、多くの/ほとんどのクラスでそれを使用せずに改善できます。」(C ++プログラミング言語Fourth Edition、Sec 1.3.4)
一般に、すべてを対象とする基本クラスが悪い考えであるのはなぜですか?また、いつ作成するのが理にかなっていますか?
Stroustrup氏は、「すべてのクラス(Objectクラス)に一意のベースをすぐに発明しないでください。通常、多くの/ほとんどのクラスでそれを使用せずに改善できます。」(C ++プログラミング言語Fourth Edition、Sec 1.3.4)
一般に、すべてを対象とする基本クラスが悪い考えであるのはなぜですか?また、いつ作成するのが理にかなっていますか?
回答:
そのオブジェクトは機能のために何を持っているのですか?Javaでは、Baseクラスにあるのは、toString、hashCodeと等式、およびmonitor + condition変数のみです。
ToStringはデバッグにのみ役立ちます。
hashCodeは、ハッシュベースのコレクションに格納する場合にのみ役立ちます(C ++の優先事項は、ハッシュ関数をテンプレートパラメーターとしてコンテナに渡すか、std::unordered_*
完全に回避し、代わりにstd::vector
プレーンな順序なしリストを使用することです)。
コンパイル時にベースオブジェクトのない等式が役立ちます。同じ型を持たない場合、等号にすることはできません。C ++では、これはコンパイル時エラーです。
モニターおよび条件変数は、ケースバイケースで明示的に含める方が適切です。
ただし、必要なことがさらにある場合は、ユースケースがあります。
たとえば、QTには、QObject
スレッドアフィニティ、親子所有権階層、およびシグナルスロットメカニズムの基礎を形成するルートクラスがあります。また、QObjectのポインターによる使用を強制しますが、Qtの多くのクラスはシグナルスロット(特に一部の説明の値型)を必要としないため、QObjectを継承しません。
Object
。
_hashCode
「別のコンテナを使用する」のではなく指していませんstd::unordered_map
実装を提供するために要素クラス自体を必要とする代わりに、C ++ がテンプレート引数を使用してハッシュを行うことを確認してください。つまり、C ++の他のすべての優れたコンテナーおよびリソースマネージャーと同様に、侵入的ではありません。誰かが後で何らかのコンテキストでそれらを必要とする可能性がある場合に備えて、すべてのオブジェクトを関数またはデータで汚染しません。
すべてのオブジェクトで共有される機能がないためです。このインターフェイスには、すべてのクラスにとって意味のあることは何もありません。
オブジェクトの高い継承階層を構築するたびに、壊れやすい基本クラス(Wikipedia)の問題に遭遇する傾向があります。
多くの小さな個別の(別個の、分離された)継承階層があると、この問題に遭遇する可能性が低くなります。
すべてのオブジェクトを1つの巨大な継承階層の一部にすることで、実際にこの問題に遭遇することが保証されます。
cout.print(x).print(0.5).print("Bye\n")
-それは依存しませんoperator<<
。
なぜなら:
あらゆる種類のvirtual
機能を実装すると、仮想テーブルが導入されますが、仮想テーブルでは、オブジェクトごとのスペースオーバーヘッドが必要になります。
toString
非仮想的に実装することは、Javaの場合とは異なり、非常にユーザーフレンドリーであり、呼び出し元が既にアクセスしているオブジェクトのアドレスのみを返すため、ほとんど役に立たないでしょう。
同様に、非仮想equals
またはhashCode
アドレスのみを使用してオブジェクトを比較できますが、これはかなり役に立たず、しばしば完全に間違っています-Javaとは異なり、オブジェクトはC ++で頻繁にコピーされるため、オブジェクトの「同一性」を区別することさえできません常に有意義または有用です。(たとえば、int
実際にはその値以外の同一性を持たせてはいけません。同じ値の2つの整数は等しくなければなりません。)
shared_ptr<Foo>
それでもあるかどうかを確認するためにshared_ptr<Bar>
(他のポインタ型または同様に)しても、Foo
そしてBar
お互いについて何も知らない無関係なクラスがあります。そのようなものが「生のポインタ」で動作することを要求することは、そのようなものがどのように使用されるかの歴史を考えると、高価になりますが、とにかくヒープに保存されるものについては、追加コストは最小限になります。
1つのルートオブジェクトがあると、多くの見返りなしに、実行できることとコンパイラーが実行できることを制限します。
共通のルートクラスを使用すると、任意のコンテナーを作成し、dynamic_cast
でその内容を抽出することがboost::any
できますが、任意のコンテナーが必要な場合は、共通のルートクラスがなくても同様のことができます。そしてboost::any
また、プリミティブをサポートしています-それは小さくても、バッファの最適化をサポートし、Javaの用語ではほとんど「箱なし」、それらを残すことができます。
C ++は、値型をサポートし、成功しています。リテラル、およびプログラマーが作成した値型。C ++コンテナは、値型を効率的に保存、ソート、ハッシュ、消費、生成します。
継承、特にモノリシックな継承Javaスタイルの基本クラスの種類は、フリーストアベースの「ポインター」または「参照」型を必要とします。データへのハンドル/ポインター/参照は、クラスのインターフェイスへのポインターを保持し、多態的に他の何かを表すことができます。
これはいくつかの状況で役立ちますが、「共通の基本クラス」を使用してパターンと結婚すると、コードパターン全体が有用でなくても、このパターンのコストと手荷物に縛られることになります。
ほとんどの場合、呼び出し側のサイトまたはそれを使用するコードのいずれかで、「オブジェクトである」というよりも、型について多くのことを知っています。
関数が単純な場合、関数をテンプレートとして記述すると、呼び出し側の情報が破棄されない、アヒル型のコンパイル時間ベースのポリモーフィズムが得られます。関数がより複雑な場合は、型の消去を実行して、実行する型の統一操作(たとえば、シリアル化と逆シリアル化)を構築し、保存して(コンパイル時に)消費する(実行時に)ことができます。別の翻訳単位のコード。
すべてをシリアル化できるようにするライブラリがあるとします。1つのアプローチは、基本クラスを持つことです。
struct serialization_friendly {
virtual void write_to( my_buffer* ) const = 0;
virtual void read_from( my_buffer const* ) = 0;
virtual ~serialization_friendly() {}
};
今、あなたが書くコードのすべてのビットが可能ですserialization_friendly
。
void serialize( my_buffer* b, serialization_friendly const* x ) {
if (x) x->write_to(b);
}
を除いてstd::vector
、すべてのコンテナを記述する必要があります。そして、そのbignumライブラリから取得した整数ではありません。そして、あなたが書いたタイプではなく、シリアル化が必要だとは思わなかった。そしてないtuple
、またはint
あるいはdouble
、またはstd::ptrdiff_t
。
別のアプローチを取ります。
void write_to( my_buffer* b, int x ) {
b->write_integer(x);
}
template<class T,
class=std::enable_if_t< void_t<
std::declval<T const*>()->write_to( std::declval<my_buffer*>()
> >
>
void write_to( my_buffer* b, T const* x ) {
if (x) x->write_to(b);
}
template<class T>
void serialize( my_buffer* b, T const& t ) {
write_to( b, t );
}
一見、何もしないことで構成されています。これを除き、型の名前空間内の自由な関数または型のメソッドとしてwrite_to
オーバーライドすることで拡張できますwrite_to
。
型の消去コードを少し書くこともできます:
namespace details {
struct can_serialize_pimpl {
virtual void write_to( my_buffer* ) const = 0;
virtual void read_from( my_buffer const* ) = 0;
virtual ~can_serialize_pimpl() {}
};
}
struct can_serialize {
void write_to( my_buffer* b ) const { pImpl->write_to(b); }
void read_from( my_buffer const* b ) { pImpl->read_from(b); }
std::unique_ptr<details::can_serialize_pimpl> pImpl;
template<class T> can_serialize(T&&);
};
namespace details {
template<class T>
struct can_serialize : can_serialize_pimpl {
std::decay_t<T>* t;
void write_to( my_buffer*b ) const final override {
serialize( b, std::forward<T>(*t) );
}
void read_from( my_buffer const* ) final override {
deserialize( b, std::forward<T>(*t) );
}
can_serialize(T&& in):t(&in) {}
};
}
template<class T> can_serialize::can_serialize<T>(T&&t):pImpl(
std::make_unique<details::can_serialize<T>>( std::forward<T>(t) );
) {}
そして今、私たちは任意の型を取り、can_serialize
インターフェースに自動ボックス化してserialize
、後で仮想インターフェースを通して呼び出すことができるようにします。
そう:
void writer_thingy( can_serialize s );
の代わりに、シリアル化できるものをすべて取る関数です
void writer_thingy( serialization_friendly const* s );
1つ目は2つ目とは異なりint
、をstd::vector<std::vector<Bob>>
自動的に処理できます。
特にこのようなことはめったにしたくないので、それを書くのに多くはかかりませんでしたが、ベース型を必要とせずにシリアル化できるものとして扱うことができるようになりました。
さらに、std::vector<T>
単にオーバーライドwrite_to( my_buffer*, std::vector<T> const& )
することで、ファーストクラスの市民としてシリアライズ可能にすることができます-そのオーバーロードで、それをvtableに格納しcan_serialize
、のシリアライズstd::vector
可能性をvtableに保存してアクセスできます.write_to
。
要するに、C ++は十分に強力であるため、必要なときに強制継承階層の価格を支払うことなく、必要に応じてオンザフライで単一の基本クラスの利点を実装できます。また、単一のベース(偽造されているかどうかに関係なく)が必要になるのは、かなりまれです。
タイプが実際にそれらのアイデンティティであり、あなたがそれらが何であるかを知っているとき、最適化の機会はたくさんあります。データはローカルに連続して保存されます(これは現代のプロセッサでのキャッシュの使いやすさにとって非常に重要です)。反対側)命令を最適に並べ替えることができ、丸穴に打たれる丸釘が少なくなります。
上記には多くの良い答えがありますが、@ ratchetfreakの答えとそのコメントに示されているように、すべてのオブジェクトの基本クラスで行うことは他の方法でより良くできるという明確な事実は非常に重要ですが、別の理由があります。それは、継承ダイヤモンドの作成を避けることです多重継承が使用される場合。ユニバーサル基本クラスに何らかの機能がある場合、多重継承の使用を開始するとすぐに、継承チェーンの異なるパスで異なるオーバーロードが発生する可能性があるため、アクセスするバリアントの指定を開始する必要があります。また、ベースを仮想化することはできません。これは非常に非効率的であるためです(すべてのオブジェクトが、メモリ使用量とローカリティの潜在的に莫大なコストで仮想テーブルを持つ必要があります)。これは、すぐに物流上の悪夢になります。
実際、Microsoftの初期のC ++コンパイラとライブラリ(16ビットのVisual C ++について知っている)には、という名前のクラスがありましたCObject
。
ただし、この時点では「テンプレート」はこの単純なC ++コンパイラではサポートされていなかったため、次のようなクラスstd::vector<class T>
は不可能だったことを知っておく必要があります。代わりに、「ベクター」実装では1つのタイプのクラスしか処理できないため、std::vector<CObject>
今日に匹敵するクラスがありました。CObject
ほぼすべてのクラスの基本クラスであったため(残念ながら、最新のコンパイラのCString
同等でstring
はありません)、ほぼすべての種類のオブジェクトを格納するためにこのクラスを使用できます。
最新のコンパイラはテンプレートをサポートしているため、この「汎用ベースクラス」の使用例は提供されなくなりました。
このようなジェネリック基本クラスを使用すると、メモリとランタイムが(少しだけ)コストがかかるという事実を考慮する必要があります(たとえば、コンストラクターの呼び出しなど)。そのため、このようなクラスを使用する場合には欠点がありますが、少なくとも最新のC ++コンパイラを使用する場合には、そのようなクラスのユースケースはほとんどありません。
TObject
MFCが存在する前から独自の機能がありました。設計のその部分をマイクロソフトのせいにしないでください。その頃のほとんどの人にとって、それは良い考えのように思えました。
Javaに由来する別の理由を提案します。
少なくともボイラープレートがなければ、すべての基本クラスを作成できないからです。
あなたはあなた自身のクラスのためにそれで逃げることができるかもしれません-しかし、あなたはおそらくあなたが多くのコードを複製することになってしまうでしょう。たとえば、「std::vector
実装されていないため、ここでは使用できません。正しいことを行うIObject
新しい派生IVectorObject
を作成した方がよいでしょう...」。
これは、組み込みまたは標準ライブラリクラスまたは他のライブラリのクラスを扱う場合に当てはまります。
今、それはあなたがのようなもので終わるだろう言語に組み込まれた場合、Integer
およびint
Javaであり、混乱、または言語の構文に大きな変化。(他の言語はすべてのタイプに組み込むことで素晴らしい仕事をしたと思います-ルビーはより良い例のようです。)
また、基本クラスがランタイムポリモーフィックでない場合(つまり、仮想関数を使用している場合)、フレームワークなどの特性を使用することでも同じメリットが得られることに注意してください。
たとえば.toString()
、次のようにすることができます:(注:既存のライブラリなどを使用してこの整頓を行うことができることを知っています、それは単なる例です。)
template<typename T>
struct ToStringTrait;
template<typename T>
std::string toString(const T & t) {
return ToStringTrait<T>::toString(t);
}
template<>
struct ToStringTrait<int> {
std::string toString(int v) {
return itoa(v);
}
}
template<typename T>
struct ToStringTrait<std::vector<T>> {
std::string toString(const std::vector<T> &v) {
std::stringstream ss;
ss<<"{";
for(int i=0; i<v.size(); ++i) {
ss<<toString(v[i]);
}
ss<<"}";
return ss.str();
}
}
おそらく「void」は、ユニバーサルベースクラスの多くの役割を果たします。任意のポインターをにキャストできますvoid*
。その後、これらのポインターを比較できます。static_cast
元のクラスに戻ることができます。
しかし、あなたがすることができないでくださいvoid
あなたが行うことができますことはObject
、あなたが本当に持っているオブジェクトの種類を把握するためにRTTIを使用しています。これは最終的に、C ++のすべてのオブジェクトがRTTIを持っているわけではなく、実際に幅ゼロのオブジェクトを持つことも可能です。
[[no_unique_address]]
。これは、コンパイラがメンバーサブオブジェクトにゼロ幅を与えるために使用できます。
[[no_unique_address]]
ことは、コンパイラがEBOメンバー変数を許可するということです。
Javaは、「未定義の動作」が存在してはならないという設計哲学を採用しています。次のようなコード:
Cat felix = GetCat();
Woofer Rover = (Woofer)felix;
Rover.woof();
interfaceを実装felix
するサブタイプを保持しているかどうかをテストしCat
ますWoofer
。実行されているwoof()
場合、キャストを実行して呼び出し、実行されていない場合は例外をスローします。 コードの動作は、felix
実装するかどうWoofer
かに関係なく完全に定義されます。
C ++は、プログラムが何らかの操作を試みてはならない場合、その操作が試みられた場合に生成されたコードが何をするかは問題ではなく、コンピューターは「あるべき」場合の動作を制限しようとして時間を無駄にしないという哲学を採用しています決して起こらない。C ++では、a *Cat
をa にキャストするために適切な間接演算子を追加すると*Woofer
、キャストは正当な場合に定義された動作を生成しますが、そうでない場合には未定義の動作を生成します。
物事に共通の基本型を持たせることで、その基本型の派生物の間でキャストを検証したり、キャスト試行操作を実行したりすることができますが、キャストの検証は、それらが正当であり、悪いことが起こらないと単純に想定するよりもコストがかかります。C ++の哲学では、そのような検証には「(通常は)必要のないものに対する支払い」が必要であるというものです。
C ++に関連するが、新しい言語の問題ではない別の問題は、複数のプログラマがそれぞれ共通のベースを作成し、それから独自のクラスを派生し、その共通のベースクラスのことを処理するコードを記述する場合、そのようなコードは、異なる基本クラスを使用したプログラマーによって開発されたオブジェクトを使用できません。新しい言語ですべてのヒープオブジェクトに共通のヘッダー形式が必要であり、許可されていないヒープオブジェクトを許可したことがない場合、そのようなヘッダーを持つヒープオブジェクトへの参照を必要とするメソッドは、任意のヒープオブジェクトへの参照を受け入れます作成することができます。
個人的には、オブジェクトに「タイプXに変換可能か」と尋ねる一般的な手段があることは、言語/フレームワークの非常に重要な機能であると思いますが、そのような機能が最初から言語に組み込まれていない場合、それは困難です後で追加します。個人的には、このような基本クラスは最初に標準ライブラリに追加する必要があり、多態的に使用されるすべてのオブジェクトはその基本から継承することを強く推奨します。プログラマーがそれぞれ独自の「基本型」を実装すると、異なる人々のコード間でオブジェクトを渡すのが難しくなりますが、多くのプログラマーが継承した共通の基本型を使用すると簡単になります。
補遺
テンプレートを使用して、「任意のオブジェクトホルダー」を定義し、そこに含まれるオブジェクトのタイプについて尋ねることができます。Boostパッケージには、そのようなものが含まれていany
ます。したがって、C ++には標準の「何かに対する型チェック可能な参照」型はありませんが、作成することは可能です。これは、言語標準に何かがないという前述の問題、つまり異なるプログラマーの実装間の非互換性を解決するものではありませんが、すべてが派生するベース型がなくてもC ++がどのように到達するかを説明します。何かのように振る舞います。
Woofer
がインターフェースでCat
あり、継承可能な場合、キャストはWoofingCat
から継承Cat
および実装するが存在する可能性があるため(現在ではない場合、将来的に)正当であると考えられますWoofer
。Javaのコンパイル/リンクモデルでは、aの作成WoofingCat
にはのソースコードへのアクセスは必要ありCat
ませんWoofer
。
Cat
へのキャストの試行を適切に処理し、Woofer
「X型に変換可能ですか」という質問に答えます。C ++を使用すると、キャストを強制することができます。ちょっとしたことがあるかもしれません。実際に何をしているのかを実際に知っているかもしれませんが、それがあなたが本当にやりたいことではない場合にも役立ちます。
dynamic_cast
、多態性オブジェクトを指す場合はポインタを渡すことで定義された動作を、そうでない場合は未定義の動作を定義します。そのため、セマンティックの観点から...