例:基本クラスBase
と3つのサブクラスがあり、すべて独自のバージョンのdoSomething()
を実装しています。
中間関数f(Base b)
では、渡されたオブジェクトに応じて、関連するバージョンのdoSomething()
を呼び出します。
doSomething
をBase
の仮想関数として宣言する必要があるのはなぜですか?確かに、b
は正しいオブジェクトを指しているので、b.doSomething
を呼び出すときに、どのメソッドを呼び出す必要があるかがわかります。
補足質問:b
が単にオブジェクトを指しているのに、型が必要なのはなぜですか?
奇妙なことに、これはかつては完全に理にかなっていたが、私は疑問を持ち始めており、疑問が生じている…
具体的には、仮想メソッドは、呼び出された特定のオブジェクトの正しいメソッドがどこにあるかを見つけるために、ポインターを逆参照する必要があります。呼び出しごとに1つのポインター逆参照はあまり聞こえないかもしれませんが、これにより関数がインライン化されることもなくなり、仮想関数呼び出しが最初に機能するためには、ポインターまたは参照を介してオブジェクト自体を参照する必要があります(それ以外の場合より広い影響を与える可能性のある「オブジェクトスライス」を取得します)。
通常、これは実際にはそれほど大きなコストではありません。そのため、Javaのような一部の言語は、「すべてのクラス変数参照を作成する」や「デフォルトですべてのメソッドを仮想化する」などの問題を回避できます)、おそらく、各言語機能が通常どのように実装されているかを詳細に理解しなくても開発できるアプリケーション用のより単純な言語を作成することを期待しています。
副次的な質問:「b」が単にオブジェクトを指している場合、なぜタイプが必要なのですか?
本当に必要な場合は、void*
を使用できます。これは、事実上、型のないポインターです。ただし、適切に型指定されたポインターは型安全性を少し追加するため、通常はこれを行うべきではありません。そして、ほとんどの場合、std::unique_ptr
のようなスマートポインタを使用する必要があります。これらのポインタは、使い終わったときに指すメモリの割り当てを自動的に解除するため、さらに安全です。
Javaに関しては、前提に完全に欠陥があります。関数が仮想であることを指定する必要はありません。すべての関数はデフォルトで仮想です。 Javaはfinal
を提供して、メソッドをオーバーライドできないことを指定します。これは多少似た効果がありますが、実際には同一ではありません。final
が「仮想ではない」ことを意味する限り、それはそれでも反対方向に実行されます。基本的には、「オプトイン」ではなく「オプトアウト」の状況です。
オブジェクトの設計方法についてBjarneが常に強調してきたことの1つは、不変条件の確立と維持です。これらの不変条件の一部は、「この値は1から100の間でなければならない」、「この値はその値の2から3倍の間でなければならない」などの値と関係があります。
ただし、オブジェクトの動作に関して不変条件を確立して維持することも同様に重要です。これらの不変条件は、特定の部分が変化する可能性のあるフレームワークを確立します。本当に醜いプログラミングではなく、プログラマーはコンパイラーのタイプチェックメカニズムを介して特定の動作を指定および強制できます。
たとえば、C++ベクトルについて考えてみましょう。 C++標準は、C++ベクトルが償却された一定の成長を提供することを保証します。ベクトルの成長関数をオーバーライドできるようにした場合、vector
から派生したクラスはその制約に違反する可能性がありますが、(仮想的に)vector
から公に派生しているため、依存する可能性のあるコードを含め、ベクトルが必要な場所であればどこでも使用できます。その制約が適用されます。
関数を仮想化することも、継承の使用を目的としています。 Bjarneは、多くのプログラマが継承を使いすぎていることを(これまで穏やかに)長年指摘してきました。仮想関数をまったく必要としない、または使用しないクラスを設計することはまったく問題ありません。1。
したがって、C++でのこの決定は、仮想関数を呼び出すオーバーヘッドを回避するために完全に(または主に)決定するものではありません。ほとんどの場合、基本的な設計と、不変条件を適用するオブジェクトについてです。これらの不変条件の多くはオブジェクトのデータにありますが、一部はオブジェクトの動作にもあります。不変条件を適用することの重要性を考えると、関数shouldはデフォルトで非仮想であるため、不変条件はデフォルトで適用され、動作は意図された場所でのみ変更できます。
1.たとえば、彼の 2003年のBill Vennersへのインタビュー について考えてみます。
パフォーマンスなどは別として.。
基本クラスにメソッドdoSomethingがあるとします。派生クラスがdoSomethingをまったく気にしないと仮定します。開発者はそれが存在することさえ忘れています。派生クラスの開発者は、何かを実行するメソッドを作成し、それをdoSomething、dosomething、do_somethingなどと呼びたいと考えています。
基本クラスのdoSomethingとはまったく関係のない新しいメソッドがdoSomethingとも呼ばれる場合、問題が発生します。言語がその問題を回避するために何かできるなら、それは素晴らしいことです。
または、逆に、開発者がdoSomethingをオーバーライドしたいが、名前に間違ったスペルを誤って使用していると仮定します。繰り返しになりますが、言語がその問題を回避するために何かできるなら、それは素晴らしいことです。
確かに「b」は正しいオブジェクトを指しているので、b.doSomethingを呼び出すときに、どのメソッドを呼び出す必要があるかがわかります。
絶対にありません!
重要な問題は、b
が基本クラスをポイントまたは参照しているが、実際には派生クラスである場合、それは、派生クラスで定義されているdoSomething
メソッドであり、 doSomething
メソッドは基本クラスです。
一部の言語は、常にそのような手荷物を持ち歩き、すべての関数をC++仮想関数と同等にします。 C++では、荷物を持ち歩く必要がないという選択肢があります。非仮想メソッドでその手荷物を持ち歩かないことの利点は、決定がコンパイル時の決定になることです。コンパイラとリンカは、どの関数を呼び出すべきかを正確に知っています。実行時のコストはゼロです。
ランタイム中に呼び出す関数を決定するオーバーヘッドは、問題の関数が呼び出される場合は問題になりませんが、1時間に数回です。関数が毎秒数万回(またはそれ以上)呼び出された場合、ランタイムのオーバーヘッドは大量に蓄積されます。