多型の理解/要件
ポリモーフィズムを理解するには(コンピューティングサイエンスでこの用語が使用されているため)、単純なテストとその定義から始めることが役立ちます。考慮してください:
Type1 x;
Type2 y;
f(x);
f(y);
ここで、f()
いくつかの動作を実行することで、値が与えられているx
と、y
入力として。
ポリモーフィズムを示すにf()
は、少なくとも2つの異なる型(例:int
およびdouble
)の値を操作して、異なる型に適したコードを見つけて実行できる必要があります。
ポリモーフィズムのためのC ++メカニズム
明示的なプログラマー指定のポリモーフィズム
f()
次のいずれかの方法で複数の型を操作できるように記述できます。
前処理:
#define f(X) ((X) += 2)
// (note: in real code, use a longer uppercase name for a macro!)
オーバーロード:
void f(int& x) { x += 2; }
void f(double& x) { x += 2; }
テンプレート:
template <typename T>
void f(T& x) { x += 2; }
仮想ディスパッチ:
struct Base { virtual Base& operator+=(int) = 0; };
struct X : Base
{
X(int n) : n_(n) { }
X& operator+=(int n) { n_ += n; return *this; }
int n_;
};
struct Y : Base
{
Y(double n) : n_(n) { }
Y& operator+=(int n) { n_ += n; return *this; }
double n_;
};
void f(Base& x) { x += 2; } // run-time polymorphic dispatch
その他の関連メカニズム
コンパイラが提供する組み込み型、標準変換、およびキャスト/強制変換用のポリモーフィズムについては、完全を期すために後で説明します。
- とにかく、彼らは一般的に直感的に理解されています(「ああ、あの」反応が必要です)。
- これらは、上記のメカニズムを必要とするしきい値、および使用のシームレスさに影響を与えます。
- 説明は、より重要な概念からの煩わしい気晴らしです。
用語
さらなる分類
上記の多態性メカニズムを考えると、それらをさまざまな方法で分類できます。
多相型固有のコードはいつ選択されますか?
- 実行時とは、コンパイラーが実行中にプログラムが処理する可能性のあるすべてのタイプのコードを生成する必要があり、実行時に正しいコードが選択される(仮想ディスパッチ)
- コンパイル時とは、コンパイル中に型固有のコードを選択することを意味します。この結果:引数
f
をint
指定して上記でのみ呼び出されたプログラムを言う-使用される多態性メカニズムとインライン化の選択に応じて、コンパイラーはのコードの生成を回避するかf(double)
、生成されたコードがコンパイルまたはリンクのある時点で破棄される可能性があります。(仮想ディスパッチを除く上記のすべてのメカニズム)
どのタイプがサポートされていますか?
- アドホックは、各タイプをサポートする明示的なコードを提供することを意味します(例:オーバーロード、テンプレートの特殊化)。「アドホック」の意味である「これのための」タイプのサポート、他の「これ」、そしておそらく「それ」のサポートも明示的に追加します;-)。
パラメトリックな意味は、サポートを有効にするために特に何もしなくても、さまざまなパラメータータイプ(例:テンプレート、マクロ)に対して関数を使用することを試みることです。テンプレート/マクロが1を 期待するように機能する関数/演算子を持つオブジェクトは、テンプレート/マクロがその仕事をするために必要なすべてであり、正確な型は無関係です。C ++ 20によって導入された「概念」は、そのような期待を表現し、強制します。ここのcppreferenceページを参照してください。
パラメトリック多態性はアヒルのタイピングを提供します。ジェームズウィットコムライリーが「アヒルのように歩いて、アヒルのように泳いでいる鳥やアヒルのように鳴く鳥を見るとき、私はその鳥をアヒルと呼びます」と言った概念です。。
template <typename Duck>
void do_ducky_stuff(const Duck& x) { x.walk().swim().quack(); }
do_ducky_stuff(Vilified_Cygnet());
サブタイプ(別名インクルージョン)ポリモーフィズムにより、アルゴリズム/関数を更新せずに新しいタイプで作業できますが、同じ基本クラス(仮想ディスパッチ)から派生する必要があります
1-テンプレートは非常に柔軟です。 SFINAE(も参照std::enable_if
)は、パラメトリック多態性に対する期待のいくつかのセットを効果的に許可します。たとえば、データの種類は、あなたしている処理がある場合ことをエンコードする可能性がある.size()
メンバーをあなたが、必要はありませんそれ以外の場合は、別の機能を一つの関数を使用します.size()
(ただし、おそらく何らかの形で苦しんでいる-低速の使用例strlen()
またはとして印刷しませんログの有用なメッセージ)。また、テンプレートが特定のパラメーターでインスタンス化されるときのアドホック動作を指定して、一部のパラメーターをパラメトリックのままにする(部分的なテンプレートの特殊化)またはそうでない(完全な特殊化)こともできます。
「ポリモーフィック」
Alf Steinbachは、C ++標準では、ポリモーフィックは仮想ディスパッチを使用した実行時のポリモーフィズムのみを指すとコメントしています。一般的な比較 サイエンス。C ++の作成者であるBjarne Stroustrupの用語集(http://www.stroustrup.com/glossary.html)のとおり、意味はより包括的です。
ポリモーフィズム-異なるタイプのエンティティへの単一のインターフェースを提供します。仮想関数は、基本クラスによって提供されるインターフェースを通じて動的(実行時)ポリモーフィズムを提供します。オーバーロードされた関数とテンプレートは、静的(コンパイル時)ポリモーフィズムを提供します。TC ++ PL 12.2.6、13.6.1、D&E 2.9。
この回答は、質問と同様に、C ++機能をCompに関連付けています。サイエンス。用語。
討論
C ++標準では、「多態性」の定義はコンプよりも狭くなっています。サイエンス。コミュニティは、のために相互理解を確実にするために、あなたの聴衆を検討します...
- 明確な用語を使用して(「このコードを他のタイプで再利用できるようにすることはできますか」または「仮想ディスパッチを使用できますか」ではなく、「このコードをポリモーフィックにすることはできますか」)および/または
- 用語を明確に定義します。
それでも、優れたC ++プログラマになるために重要なことは、ポリモーフィズムが実際に何をしているかを理解することです...
「アルゴリズム」コードを一度記述してから、それを多くのタイプのデータに適用できるようにする
...そして、さまざまな多態性メカニズムが実際のニーズにどのように一致するかを十分に認識します
ランタイムポリモーフィズムスーツ:
- 入力はファクトリメソッドによって処理され、
Base*
s を介して処理される異種オブジェクトコレクションとして生成されます。
- 実行時に構成ファイル、コマンドラインスイッチ、UI設定などに基づいて選択された実装
- 実装は、ステートマシンパターンなど、実行時に変化しました。
ランタイムポリモーフィズムの明確なドライバーがない場合は、コンパイル時のオプションが望ましい場合がよくあります。考慮してください:
- テンプレートクラスのコンパイルと呼ばれる側面は、実行時に失敗する太いインターフェイスよりも望ましい
- SFINAE
- CRTP
- 最適化(インライン化とデッドコードの除去、ループのアンロール、静的なスタックベースの配列とヒープを含む多く)
__FILE__
、__LINE__
文字列リテラルの連結と(悪まま;-))マクロの他のユニークな機能
- テンプレートとマクロテストのセマンティック使用法がサポートされていますが、そのサポートの提供方法を人為的に制限していません(正確に一致するメンバー関数のオーバーライドを要求することで仮想ディスパッチが行われる傾向があるため)
多型をサポートするその他のメカニズム
約束どおり、完全を期すためにいくつかの周辺トピックがカバーされています。
- コンパイラーが提供するオーバーロード
- 変換
- キャスト/強制
この回答は、上記を組み合わせてポリモーフィックコード、特にパラメトリックポリモーフィズム(テンプレートとマクロ)を強化および簡略化する方法の説明で終わります。
タイプ固有の操作にマッピングするためのメカニズム
>コンパイラが提供する暗黙のオーバーロード
概念的には、コンパイラは組み込み型の多くの演算子をオーバーロードします。ユーザー指定のオーバーロードと概念的には違いはありませんが、見落とされやすいためリストされています。たとえば、同じ表記法を使用してint
sとdouble
sに追加するx += 2
と、コンパイラーは以下を生成します。
その後、オーバーロードはシームレスにユーザー定義型に拡張されます。
std::string x;
int y = 0;
x += 'c';
y += 'c';
コンパイラーが提供する基本型のオーバーロードは、高水準(3GL +)のコンピューター言語では一般的であり、ポリモーフィズムの明示的な議論は、一般的にはもっと何かを意味します。(2GL-アセンブリ言語-多くの場合、プログラマーは異なる型に対して異なるニーモニックを明示的に使用する必要があります。)
>標準変換
C ++標準の4番目のセクションでは、標準変換について説明します。
最初のポイントはうまくまとめられています(古いドラフトから-うまくいけばまだ実質的に正しい):
-1-標準変換は、組み込み型に対して定義された暗黙の変換です。節convは、そのような変換の完全なセットを列挙します。標準変換シーケンスは、次の順序での標準変換のシーケンスです。
[注:標準変換シーケンスは空にすることができます。つまり、変換なしで構成できます。]必要な宛先タイプに変換する必要がある場合は、標準の変換シーケンスが式に適用されます。
これらの変換により、次のようなコードが可能になります。
double a(double x) { return x + 2; }
a(3.14);
a(42);
以前のテストを適用する:
ポリモーフィックであるためには、[ a()
]は少なくとも2つの異なる型(int
およびdouble
)の値を操作でき、型に適したコードを見つけて実行できる必要があります。
a()
それ自体は特にコードを実行するdouble
ため、多態的ではありません。
ただし、a()
コンパイラへの2回目の呼び出しでは、「浮動小数点昇格」(標準§4)の型に適したコードを生成してに変換することがわかっ42
てい42.0
ます。その追加のコードは呼び出し元の関数にあります。結論として、この重要性について説明します。
>強制、キャスト、暗黙のコンストラクタ
これらのメカニズムにより、ユーザー定義のクラスは、組み込み型の標準変換と同様の動作を指定できます。みてみましょう:
int a, b;
if (std::cin >> a >> b)
f(a, b);
ここでは、オブジェクトstd::cin
は、変換演算子を使用してブールコンテキストで評価されます。これは、上記のトピックの標準変換からの「統合プロモーション」などと概念的にグループ化できます。
暗黙的なコンストラクターは事実上同じことを行いますが、キャスト先の型によって制御されます。
f(const std::string& x);
f("hello"); // invokes `std::string::string(const char*)`
コンパイラーが提供するオーバーロード、変換、および強制の影響
考慮してください:
void f()
{
typedef int Amount;
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
x
除算中に金額を実数として処理する場合(つまり、6に切り捨てるのではなく6.5にする)、必要なのはに変更することだけですtypedef double Amount
。
それはうれしいですが、なかったであろう、あまりにもコードが明示的に「正しい入力」にする多くの仕事:
void f() void f()
{ {
typedef int Amount; typedef double Amount;
Amount x = 13; Amount x = 13.0;
x /= 2; x /= 2.0;
std::cout << double(x) * 1.1; std::cout << x * 1.1;
} }
ただし、最初のバージョンをに変換できることを考慮してtemplate
ください。
template <typename Amount>
void f()
{
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
これらの小さな「便利な機能」が原因で、int
またはのいずれかで簡単にインスタンス化してdouble
、意図したとおりに機能させることができます。これらの機能がなければ、明示的なキャスト、型の特性、ポリシークラス、または次のような冗長でエラーが発生しやすい混乱が必要になります。
template <typename Amount, typename Policy>
void f()
{
Amount x = Policy::thirteen;
x /= static_cast<Amount>(2);
std::cout << traits<Amount>::to_double(x) * 1.1;
}
したがって、組み込み型のコンパイラ提供の演算子オーバーロード、標準変換、キャスト/強制/暗黙コンストラクタ-これらはすべて、ポリモーフィズムの微妙なサポートに貢献しています。この回答の冒頭の定義から、次のマッピングによって「型に適したコードを見つけて実行する」ことを扱います。
パラメータタイプから「離れて」
定数型の値からパラメトリック型へ
それらはそれ自体でポリモーフィックコンテキストを確立しませんが、そのようなコンテキスト内のコードを強化/簡素化するのに役立ちます。
あなたはだまされたと感じるかもしれません...それは多くのようには思えません。重要なのは、パラメトリックポリモーフィックコンテキスト(つまり、テンプレートまたはマクロ内)では、任意の広い範囲の型をサポートしようとしているが、他の関数、リテラル、およびタイプの小さなセット。操作/値が論理的に同じである場合に、タイプごとにほぼ同一の関数またはデータを作成する必要性を減らします。これらの機能が連携して「ベストエフォート」の態度を追加し、限られた使用可能な機能とデータを使用して直感的に期待されることを行い、実際のあいまいな場合にのみエラーで停止します。
これは、ポリモーフィックコードをサポートするポリモーフィックコードの必要性を制限し、ポリモーフィズムの使用についてより緊密なネットを描くことで、ローカライズされた使用が広範な使用を強制しないようにし、必要に応じてポリモーフィズムの利点を利用できるようにします。コンパイル時に、使用される型をサポートするためにオブジェクトコードに同じ論理関数の複数のコピーを含め、インライン化または少なくともコンパイル時に解決された呼び出しではなく、仮想ディスパッチを実行します。C ++で一般的であるように、プログラマーには、ポリモーフィズムが使用される境界を制御する多くの自由が与えられます。