Stroustrupは、「すぐにすべてのクラス(オブジェクトクラス)の一意のベースを考案しないでください。通常、多くの/ほとんどのクラスでそれなしでより良いことができます。」 (C++プログラミング言語第4版、セクション1.3.4)
すべての基本クラスが一般に悪い考えである理由と、それを作成することがいつ意味があるのか?
そのオブジェクトが機能のために何を持っているからですか? Javaでは、Baseクラスに含まれるのは、toString、hashCode&equality、およびmonitor + condition変数のみです。
ToStringはデバッグにのみ役立ちます。
hashCodeは、ハッシュベースのコレクションに保存する場合にのみ役立ちます(C++の設定では、ハッシュ関数をテンプレートのパラメーターとしてコンテナーに渡すか、std::unordered_*
を完全に避け、代わりにstd::vector
を使用しますプレーンな順不同リスト)。
基本オブジェクトなしの等価性はコンパイル時に役立ちます。それらが同じ型を持っていない場合、それらを等価にすることはできません。 C++では、これはコンパイル時エラーです。
モニターおよび条件変数は、ケースバイケースで明示的に含める方が適切です。
ただし、実行する必要があることがさらにある場合は、ユースケースがあります。
たとえばQTには、ルートアフィニティ、親子の所有権階層、およびシグナルスロットメカニズムの基礎を形成するルートQObject
クラスがあります。また、QObjectのポインタによる使用を強制しますが、Qtの多くのクラスはQObjectを継承しませんは、シグナルスロット(特に、一部の説明の値の型)を必要としないためです。
すべてのオブジェクトで共有される関数がないためです。このインターフェイスには、すべてのクラスにとって意味のあるものは何もありません。
オブジェクトの高い継承階層を構築するときはいつでも、 Fragile Base Class(Wikipedia。) の問題に遭遇する傾向があります。
多くの小さな個別の(個別の、孤立した)継承階層があると、この問題が発生する可能性が低くなります。
すべてのオブジェクトを1つの巨大な継承階層の一部にすることで、この問題が発生することが実際に保証されます。
なぜなら:
anyのようなvirtual
関数を実装すると、仮想テーブルが導入されます。これには、オブジェクトごとのスペースオーバーヘッドが必要であり、多くの(ほとんどの)状況では必要も望まれもしません。
toString
を非仮想的に実装しても、Javaとは異なり、非常にユーザーフレンドリーでなく、呼び出し元がすでにアクセスできるオブジェクトのアドレスのみが返されるため、ほとんど役に立たないでしょう。
同様に、非仮想equals
またはhashCode
はオブジェクトを比較するためにアドレスを使用することしかできませんでした。 C++では、したがって、オブジェクトの「アイデンティティ」を区別することは、必ずしも意味がなく、有用でもありません。 (たとえば、int
は実際にはnotの値以外のIDを持つ必要があります...同じ値の2つの整数は等しい必要があります。)
ルートオブジェクトを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>>
を自動的に処理できます。
特にこの種のことはめったにやりたくないことなので、書くのにそれほど時間はかかりませんでしたが、基本型を必要とせずにシリアル化可能として扱うことができるようになりました。
さらに、write_to( my_buffer*, std::vector<T> const& )
をオーバーライドするだけで、std::vector<T>
をファーストクラスシチズンとしてシリアライズ可能にすることができます-そのオーバーロードを使用して、can_serialize
に渡すことができ、std::vector
のシリアライザビリティがvtableに格納され、 .write_to
によってアクセスされます。
要するに、C++は強力であり、必要なときに強制継承階層の代償を払うことなく、必要なときに単一の基本クラスの利点をオンザフライで実装できます。また、単一のベース(偽装されているかどうかにかかわらず)が必要とされる時期は、かなりまれです。
タイプが実際にはそのアイデンティティであり、タイプが何かを知っている場合、最適化の機会はたくさんあります。データはローカルに連続して保存されます(これは、最新のプロセッサでのキャッシュの使いやすさにとって非常に重要です)。コンパイラーは、特定の操作が何を実行するかを簡単に理解できます(不透明な仮想メソッドポインターが飛び越さなければならない代わりに、不明なコードが反対側)では、命令を最適に並べ替えることができ、丸い穴に打ち込まれる丸いペグが少なくなります。
上記の良い答えはたくさんありますが、@ ratchetfreakの答えとそのコメントで示されているように、base-class-of-all-objectsで行うことは他の方法でより適切に実行できるという明確な事実は重要ですが、別の理由があります。多重継承が使用されている場合、 継承ダイヤモンド の作成を回避するためです。ユニバーサルベースクラスに機能があった場合、多重継承の使用を開始するとすぐに、アクセスしたいバリアントのバリアントを指定する必要があります。継承チェーンの異なるパスで異なる方法でオーバーロードされる可能性があるためです。また、ベースを仮想化することはできません。これは非常に非効率であるためです(すべてのオブジェクトに仮想テーブルを用意する必要があり、メモリ使用量と局所性のコストが非常に高くなる可能性があります)。これはすぐにロジスティックスの悪夢になるでしょう。
実際、Microsoftの初期のC++コンパイラとライブラリ(Visual C++について知っている、16ビット)には、CObject
という名前のクラスがありました。
ただし、当時、「テンプレート」はこの単純なC++コンパイラではサポートされていなかったため、std::vector<class T>
のようなクラスは使用できなかったことを知っておく必要があります。代わりに、「ベクター」実装は1つのタイプのクラスしか処理できなかったため、今日のstd::vector<CObject>
に相当するクラスがありました。 CObject
はほぼすべてのクラスの基本クラスだったので(残念ながらCString
ではない-最近のコンパイラのstring
に相当)、このクラスを使用してほぼすべての種類のオブジェクト。
最新のコンパイラーはテンプレートをサポートしているため、「汎用基本クラス」のこの使用例は提供されなくなりました。
このような一般的な基本クラスを使用すると、メモリとランタイムが(たとえば、コンストラクタの呼び出しで)コストがかかるという事実を考慮する必要があります。したがって、そのようなクラスを使用する場合には欠点がありますが、少なくとも最新のC++コンパイラを使用する場合には、そのようなクラスのユースケースはほとんどありません。
Javaから来る別の理由を提案します。
yoはeverythingの基本クラスを作成できないため、少なくともボイラープレートの束がなければできません。
あなたはあなた自身のクラスのためにそれを回避することができるかもしれません-しかしあなたはおそらくあなたが多くのコードを複製することになるでしょう。例えば。 「_std::vector
_は、IObject
を実装していないため、ここでは使用できません。正しいことを行う新しい派生IVectorObject
を作成した方がいいです...」.
これは、組み込みライブラリ、標準ライブラリクラス、または他のライブラリのクラスを扱う場合に常に当てはまります。
これが言語に組み込まれていると、JavaでのInteger
とint
の混乱、または言語構文の大幅な変更などが発生します。 (他のいくつかの言語はすべてのタイプに組み込むことで素晴らしい仕事をしたと思います-Rubyはより良い例のようです)
また、基本クラスがランタイムのポリモーフィックでない場合(つまり、仮想関数を使用する場合)は、フレームワークなどの特性を使用することで同じ利点が得られることにも注意してください。
例えば.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*
へのポインタをキャストできます。その後、それらのポインターを比較できます。 static_cast
を元のクラスに戻すことができます。
しかし、あなたができないvoid
でできることObject
でできることは、RTTIを使用して、実際に持っているオブジェクトのタイプを把握することです。これは、最終的にはC++のすべてのオブジェクトがRTTIを備えているわけではなく、実際に幅がゼロのオブジェクトを持つことが可能です。
Javaは未定義の動作は存在してはならないという設計哲学を採用しています。次のようなコード:
_Cat felix = GetCat();
Woofer Rover = (Woofer)felix;
Rover.woof();
_
felix
がインターフェースCat
を実装するWoofer
のサブタイプを保持しているかどうかをテストします。含まれている場合はキャストを実行してwoof()
を呼び出し、含まれていない場合は例外をスローします。 コードの動作は、felix
がWoofer
を実装するかどうかにかかわらず完全に定義されます。
C++は、プログラムがなんらかの操作を試みるべきではない場合、その操作が試みられた場合に生成されたコードが何を行うかは問題ではなく、「すべき」場合にコンピューターが動作を制約しようとする時間を浪費してはならないという哲学を採用しています。決して発生しません。 C++では、_*Cat
_を_*Woofer
_にキャストするように適切な間接演算子を追加すると、コードはキャストが正当な場合に定義済みの動作を生成しますただし、そうでない場合は未定義の動作。
物事に共通の基本型があると、その基本型の派生物の中でキャストを検証し、キャスト操作を行うこともできますが、キャストの検証は、正当であると仮定して悪いことが起こらないことを期待するよりもコストがかかります。 C++の哲学では、このような検証には「[通常]不要なものに対する支払い」が必要です。
C++に関連するもう1つの問題は、新しい言語では問題にならないでしょう。複数のプログラマーがそれぞれ共通のベースを作成し、そこから独自のクラスを派生させ、その共通のベースクラスのもので動作するようにコードを記述するということです。このようなコードは、異なる基本クラスを使用するプログラマーが開発したオブジェクトを処理できません。新しい言語ですべてのヒープオブジェクトに共通のヘッダー形式が必要であり、そうでないヒープオブジェクトを許可していない場合、そのようなヘッダーを持つヒープオブジェクトへの参照を必要とするメソッドは、すべてのヒープオブジェクトへの参照を誰でも受け入れます。作成することができます。
個人的には、オブジェクトを「タイプXに変換可能ですか」と尋ねる一般的な手段を持つことは、言語/フレームワークでは非常に重要な機能だと思いますが、そのような機能が最初から言語に組み込まれていないと、後で追加します。個人的には、このような基本クラスは、最初に標準ライブラリに追加する必要があると思います。多態的に使用されるすべてのオブジェクトは、その基本から継承することを強くお勧めします。プログラマーがそれぞれ独自の「基本タイプ」を実装すると、さまざまな人々のコード間でオブジェクトを渡すことが難しくなりますが、多くのプログラマーが継承した共通の基本タイプを使用すると簡単になります。
[〜#〜]補遺[〜#〜]
テンプレートを使用すると、「任意のオブジェクトホルダー」を定義して、そこに含まれるオブジェクトのタイプを尋ねることができます。 Boostパッケージにはany
と呼ばれるものが含まれています。したがって、C++には標準の「型チェック可能な何かへの参照」型がないとしても、C++を作成することは可能です。これは、言語標準に何かがないという前述の問題、つまり異なるプログラマーの実装間の非互換性を解決しませんが、C++がすべての派生元である基本型なしでどのようにして成功するかを説明します:作成を可能にすることによって一つのように振る舞う何か。
Symbian C++は実際、特定の方法で動作するすべてのオブジェクト(主にそれらがヒープを割り当てた場合)に対して、ユニバーサルベースクラスCBaseを備えていました。仮想デストラクタを提供し、構築時にクラスのメモリをゼロにし、コピーコンストラクタを隠しました。
背後にある理論的根拠は、それが組み込みシステムとC++コンパイラーのための言語であり、仕様が10年前に本当にひどいものだったということです。
これから継承されたクラスはすべてではなく、一部のみです。