1. ホーム
  2. c++

[解決済み] C++のポリモーフィズム

2022-06-27 08:58:22

質問

AFAIKです。

C++は3種類のポリモーフィズムを提供します。

  • 仮想関数
  • 関数名のオーバーロード
  • 演算子のオーバーロード

上記の3種類のポリモーフィズムの他に、他の種類のポリモーフィズムが存在します。

  • 実行時
  • コンパイル時
  • アドホックなポリモーフィズム
  • パラメトリックポリモーフィズム

私は、以下のことを知っています。 実行時ポリモーフィズム が実現できるのは 仮想関数 静的ポリモーフィズム が実現できるのは テンプレート関数

しかし、他の2つについては

アドホックポリモーフィズム。

実際に使用できる型の範囲が有限で、使用する前に組み合わせを個別に指定する必要がある場合、これをアドホック多相性と呼びます。

パラメトリックポリモーフィズムです。

すべてのコードが特定の型に言及することなく書かれ、したがって任意の数の新しい型で透過的に使用できる場合、それはパラメトリックポリモーフィズムと呼ばれます。

ほとんど理解できない :(

どなたか例を挙げて説明していただけませんか? この質問への回答が、多くの新卒合格者の参考になればと思います。

どのように解決するのですか?

ポリモーフィズムの理解・要件

多相性を理解するために - この用語はコンピュータサイエンスで使われます - その簡単なテストと定義から始めるとよいでしょう。 考えてみてください。

    Type1 x;
    Type2 y;

    f(x);
    f(y);

ここで f() は何らかの操作を行うためのものであり、そのための値として xy を入力とする。

ポリモーフィズムを発揮するために f() の値を操作できなければなりません。 の値を操作できなければなりません。 型(例えば intdouble を含む)、タイプに適した明確なコードを見つけ、実行します。


ポリモーフィズムのための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) のコードを生成しないかもしれませんし、生成されたコードはコンパイルやリンクのある時点で捨てられるかもしれません。 ( 仮想ディスパッチ以外のすべてのメカニズム )



  • どのタイプに対応していますか?

    • アドホック というのは、それぞれの型をサポートするための明示的なコード (例: オーバーロード、テンプレートの特殊化) を提供し、サポート "for this" を明示的に追加することです (例:のように)。 アドホック の意味で) 型、他のいくつかの "これ"、そして多分 "あれ"も;-)のサポートを明示的に追加します。
    • パラメトリック というのは、特に何もしなくても、様々なパラメータタイプに対応した関数を使おうとするだけでよいという意味です(例:テンプレート、マクロ)。 テンプレート/マクロが期待するように動作する関数/演算子を持つオブジェクト 1 は、テンプレート/マクロがその機能を果たすために必要なものすべてであり、正確な型は関係ありません。C++20 で導入された "concepts"は、このような期待を表現し、強制するものです。 cppreference のページを参照してください。 .

      • パラメトリック・ポリモーフィズムは ダックタイピング - これは James Whitcomb Riley が言ったとされる概念で、彼はこう言ったらしい。 アヒルのように歩き、アヒルのように泳ぎ、アヒルのように鳴く鳥を見たとき、私はその鳥をアヒルと呼ぶ。 .

        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++ 標準では ポリモーフィック は仮想ディスパッチを用いた実行時ポリモーフィズムのみを指すと述べています。 一般的なComp. Sci.の意味はもっと包括的で、C++の作者であるBjarne Stroustrupの用語集( http://www.stroustrup.com/glossary.html ):

ポリモーフィズム - 異なるタイプのエンティティに単一のインターフェイスを提供すること。仮想関数は基底クラスが提供するインタフェースを通じて動的な(実行時の)ポリモーフィズムを提供します。オーバーロードされた関数とテンプレートは、静的な(コンパイル時の)ポリモーフィズムを提供します。TC++PL 12.2.6, 13.6.1, D&E 2.9に記載されています。

この回答は、質問と同様に、C++の機能をComp.Sci.の用語に関連付けます。Sci. の用語に関連しています。

ディスカッション

C++標準では、Comp. Scienceコミュニティよりも狭い範囲のポリモーフィズムの定義を使っています。Sci. コミュニティでは、以下のような相互理解を確実にするために のための相互理解を確保する必要があります。 の相互理解を確実にするために...

  • 曖昧でない用語を使うこと ("can we make this code reusable for other types?" or "can we use virtual dispatch?" rather than "can we make this code polymorphic?"), および/または
  • 用語の明確な定義。

それでも、優れたC++プログラマになるために欠かせないのは を理解することです。 を理解することです。

アルゴリズム的なコードを一度書いたら、それを多くの種類のデータに適用することができる

...そして、異なるポリモーフィック機構が実際のニーズにどのように合致するかをよく理解することです。

ランタイム・ポリモーフィズムが合う

  • ファクトリーメソッドで処理された入力は、異種オブジェクトのコレクションとして吐き出され、そのコレクションは Base* s,
  • の実装は、設定ファイル、コマンドラインスイッチ、UI設定などに基づいて実行時に選択されます。
  • ステートマシンパターンのように、実行時に変化する実装。

実行時ポリモーフィズムのための明確なドライバがない場合、コンパイル時のオプションがしばしば好まれます。 考えてみてください。

  • テンプレート化されたクラスのいわゆるコンパイル時の側面は、実行時に失敗するファットインターフェイスよりも望ましいです。
  • SFINAE
  • CRTP
  • 最適化 (インライン化およびデッドコードの排除、ループのアンロール、スタックベースの静的配列とヒープを含む多くの機能)
  • __FILE__ , __LINE__ や、文字列リテラルの連結など、マクロならではの機能があります(邪道ですが;-))。
  • テンプレートとマクロは意味的な使用がサポートされていることをテストしますが、サポートが提供される方法を人為的に制限しません (仮想ディスパッチが正確に一致するメンバー関数のオーバーライドを要求することによってそうなりがちなように)。

ポリモーフィズムをサポートする他のメカニズム

約束通り、完全性を期すため、いくつかの周辺トピックがカバーされています。

  • コンパイラが提供するオーバーロード
  • 変換子
  • キャスト/強制

この回答は、上記の組み合わせがどのようにポリモーフィックコード、特にパラメトリックポリモーフィズム(テンプレートとマクロ)に力を与え、簡素化するかを議論することで締めくくられています。

型固有の操作にマッピングするためのメカニズム

暗黙のコンパイラ提供のオーバーロード

概念的には、コンパイラが オーバーロード という演算子があります。 これはユーザー指定のオーバーロードと概念的には変わりませんが、見落とされやすいのでリストアップしました。 例えば int という演算子や double は同じ記法で x += 2 とコンパイラが生成する。

  • タイプ固有の CPU 命令
  • 同じ型の結果

オーバーロードはその後、ユーザー定義型にもシームレスに拡張されます。

std::string x;
int y = 0;

x += 'c';
y += 'c';

コンパイラが提供する基本的な型のオーバーロードは高水準(3GL+)のコンピュータ言語では一般的であり、ポリモーフィズムの明示的な議論は一般的にそれ以上のものを意味するものである。 (2GL - アセンブリ言語 - は、しばしばプログラマが異なる型のために異なるニーモニックを明示的に使用することを要求します)。

標準的な変換

C++標準の第4章では、Standardの変換について説明されています。

最初のポイントはうまく要約されています(古い草案から - うまくいけばまだ実質的に正しい)。

-1- 標準変換は組み込み型のために定義された暗黙の変換です。conv節はそのような変換の完全なセットを列挙しています。標準変換シーケンスは,以下の順序での標準変換のシーケンスです。

  • 次のセットから0または1つの変換:lvalueからrvalueへの変換、配列からポインタへの変換、関数からポインタへの変換。

  • 以下のセットから0個または1個の変換:積分プロモーション、浮動小数点プロモーション、積分変換、浮動小数点変換、浮動小数点-積分変換、ポインタ変換、ポインタからメンバーへの変換、ブール値への変換。

  • ゼロまたは1の資格変換。

[注意:標準的な変換シーケンスは空である、すなわち、変換なしで構成されることができます。] 標準的な変換シーケンスは,式を必要な目的型に変換するために必要であれば,式に適用されます。

これらの変換により、以下のようなコードが可能になります。

double a(double x) { return x + 2; }

a(3.14);
a(42);

先ほどのテストを適用する。

多相であるためには、[ a() ]は少なくとも2つの値を操作できなければなりません。 明確な 型(例えば intdouble ), タイプに適したコードを見つけ、実行する .

a() は、それ自体が double であり、したがって ではなく ポリモーフィックではありません。

しかし、2回目の呼び出しで a() を変換するために、コンパイラは "浮動小数点プロモーション" (標準§4) のための型に適したコードを生成することを知っています。 4242.0 . この余分なコードは 呼び出し 関数にあります。 このことの意義については、結論で述べます。

> 強制、キャスト、暗黙のコンストラクタ

これらのメカニズムにより、ユーザー定義クラスは組み込み型の標準変換に似た振る舞いを指定することができます。 見てみましょう。

int a, b;

if (std::cin >> a >> b)
    f(a, b);

ここでは、オブジェクト std::cin は変換演算子の助けを借りて、ブーリアンコンテキストで評価されます。 これは上のトピックのStandard conversionから"integral promotions"等と概念的にグループ化することができます。

暗黙のコンストラクタは効果的に同じことを行いますが、cast-to型によって制御されます。

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;
}

コンパイラが提供する組み込み型の演算子オーバーロード、標準変換、キャスト/強制/暗黙のコンストラクタなど、これらはすべてポリモーフィズムの微妙なサポートに寄与しているわけです。 この回答の一番上にある定義から、それらはマッピングによって型に適したコードを見つけ、実行することに対処しています。

  • パラメータ型からの "away"。

    • から ポリモーフィックなアルゴリズムコードが扱う多くのデータ型について

    • まで 同じまたは他の)型の(潜在的により少ない)数のために書かれたコード。

  • 定数型の値からパラメトリック型への変換

これらは ではない 多相コンテキストを確立するものではありませんが、そのようなコンテキストの中でコードを強化・簡略化する手助けをします。

騙されたと思うかもしれません...たいしたことないように思えますが。 重要なのは、パラメトリックな多相コンテキスト (つまり、テンプレートやマクロの内部) では、任意の大きな範囲の型をサポートしようとしていますが、しばしば、小さな型のセットのために設計された他の関数、リテラル、操作の観点から、それらの操作を表現したいと思うのです。 操作や値が論理的に同じである場合に、型ごとにほぼ同じ関数やデータを作成する必要性を減らすことができるのです。 これらの機能が協調して、限られた利用可能な関数やデータを使って直感的に期待されることを行い、本当に曖昧な場合にのみエラーで停止するという、quot;best effort" という姿勢を付加しているのです。

これは、ポリモーフィック コードをサポートするポリモーフィック コードの必要性を制限し、ポリモーフィズムの使用についてより厳重な網を張って、局所的な使用が広範囲にわたる使用を強制されないようにするのに役立ちます。 C++で一般的なように、プログラマはポリモーフィズムが使用される境界を制御するために多くの自由を与えられます。