トピックが求めるものだけです。また、CRTP の通常の例で dtor が言及されていない理由も知りたいですvirtual
。
編集:みんな、CRTPの問題についても投稿してください、ありがとう。
仮想関数のみが動的ディスパッチ (したがってvtableルックアップ) を必要とし、すべての場合でさえ必要ではありません。コンパイラがコンパイル時にメソッド呼び出しの最終的なオーバーライドを決定できる場合、実行時のディスパッチの実行を省略できます。必要に応じて、ユーザー コードで動的ディスパッチを無効にすることもできます。
struct base {
virtual void foo() const { std::cout << "base" << std::endl; }
void bar() const { std::cout << "bar" << std::endl; }
};
struct derived : base {
virtual void foo() const { std::cout << "derived" << std::endl; }
};
void test( base const & b ) {
b.foo(); // requires runtime dispatch, the type of the referred
// object is unknown at compile time.
b.base::foo();// runtime dispatch manually disabled: output will be "base"
b.bar(); // non-virtual, no runtime dispatch
}
int main() {
derived d;
d.foo(); // the type of the object is known, the compiler can substitute
// the call with d.derived::foo()
test( d );
}
継承のすべてのケースで仮想デストラクタを提供する必要があるかどうかについては、答えはノーであり、必ずしもそうではありません。仮想デストラクタが必要になるdelete
のは、基本型へのポインターを介してコードが派生型のオブジェクトを保持している場合のみです。一般的なルールは、
規則の 2 番目の部分は、ユーザー コードがベースへのポインターを介してオブジェクトを削除できないことを保証します。これは、デストラクターが仮想である必要がないことを意味します。利点は、クラスに仮想メソッドが含まれていない場合、クラスのプロパティが変更されないことです。最初の仮想メソッドが追加されると、クラスのメモリ レイアウトが変更され、vtable ポインターが保存されます。各インスタンスで。2つの理由のうち、1つ目が重要です。
struct base1 {};
struct base2 {
virtual ~base2() {}
};
struct base3 {
protected:
~base3() {}
};
typedef base1 base;
struct derived : base { int x; };
struct other { int y; };
int main() {
std::auto_ptr<derived> d( new derived() ); // ok: deleting at the right level
std::auto_ptr<base> b( new derived() ); // error: deleting through a base
// pointer with non-virtual destructor
}
main の最後の行の問題は、2 つの異なる方法で解決できます。typedef
が変更された場合base1
、デストラクタはオブジェクトに正しくディスパッチされderived
、コードは未定義の動作を引き起こしません。コストはderived
、仮想テーブルが必要になり、各インスタンスにポインターが必要になることです。さらに重要なことは、derived
とのレイアウト互換性がなくなったことother
です。もう 1 つの解決策は、 を に変更するtypedef
ことbase3
です。この場合、問題はコンパイラにその行で叫ばせることで解決されます。欠点は、ベースへのポインターを介して削除できないことです。利点は、コンパイラーが未定義の動作がないことを静的に確認できることです。
CRTP パターンの特定のケース (冗長なパターンを許してください) では、ほとんどの作成者はデストラクタを保護することさえ気にしません。これは、基本 (テンプレート化された) 型への参照によって派生型のオブジェクトを保持することが意図されていないためです。安全のために、デストラクタを保護されたものとしてマークする必要がありますが、それが問題になることはめったにありません。
本当にありそうもない。標準には、コンパイラがばかげて非効率的なクラス全体を実行するのを止めるものは何もありませんが、クラスに仮想関数があるかどうかに関係なく、非仮想呼び出しは依然として非仮想呼び出しです。動的タイプではなく、静的タイプに対応する関数のバージョンを呼び出す必要があります。
struct Foo {
void foo() { std::cout << "Foo\n"; }
virtual void virtfoo() { std::cout << "Foo\n"; }
};
struct Bar : public Foo {
void foo() { std::cout << "Bar\n"; }
void virtfoo() { std::cout << "Bar\n"; }
};
int main() {
Bar b;
Foo *pf = &b; // static type of *pf is Foo, dynamic type is Bar
pf->foo(); // MUST print "Foo"
pf->virtfoo(); // MUST print "Bar"
}
したがって、実装で非仮想関数を vtable に配置する必要はまったくありません。実際、vtable には、この例ではとBar
の 2 つの異なるスロットが必要です。つまり、実装で必要な場合でも、vtable の特殊なケースでの使用になります。実際には、それはしたくありません。それを行う意味がありません。心配する必要はありません。Foo::foo()
Bar::foo()
CRTP 基底クラスには、非仮想で保護されたデストラクタが必要です。
クラスのユーザーがオブジェクトへのポインターを取得し、それを基本クラスのポインター型にキャストしてから削除する可能性がある場合は、仮想デストラクターが必要です。仮想デストラクタは、これが機能することを意味します。基本クラスの保護されたデストラクタは、それらの試行を停止します (delete
アクセス可能なデストラクタがないため、コンパイルされません)。したがって、仮想または保護のいずれかが、ユーザーが誤って未定義の動作を引き起こすという問題を解決します。
こちらのガイドライン #4 を参照してください。この記事の「最近」とは、ほぼ 10 年前を意味することに注意してください。
http://www.gotw.ca/publications/mill18.htm
CRTP 基本クラスの目的でBase<Derived>
はないため、オブジェクトではない独自のオブジェクトを作成するユーザーはいません。Derived
デストラクタにアクセスできる必要がないだけです。したがって、公開インターフェイスから除外するか、コード行を保存するために公開のままにして、ユーザーがばかげたことをしないことに頼ることができます。
仮想である必要がないことを考えると、仮想であることが望ましくない理由は、それらを必要としない場合、クラスに仮想関数を与える意味がないということです。いつの日か、オブジェクトのサイズ、コードの複雑さ、さらには (可能性が低い) 速度の点でコストがかかる可能性があるため、物事を常に仮想化するのは時期尚早です。CRTP を使用する種類の C++ プログラマーの間で好まれるアプローチは、クラスが何のためにあるのか、それらが基本クラスとして設計されているかどうか、もしそうであれば、それらがポリモーフィック ベースとして使用されるように設計されているかどうかを完全に明確にすることです。CRTP 基本クラスはそうではありません。
ユーザーが CRTP 基本クラスへのビジネス キャストを持たない理由は、それが public であっても、実際には「より良い」インターフェイスを提供していないためです。Derived*
CRTP 基底クラスは派生クラスに依存するため、 にキャストしても、より一般的なインターフェイスに切り替えるわけではありませんBase<Derived>*
。Base<Derived>
基本クラスとしても持っていない限り、他のクラスが基本クラスとして持つことはありませんDerived
。多形ベースとしては役に立たないので、多形ベースにしないでください。
最初の質問に対する答え: いいえ。仮想関数の呼び出しのみが、実行時に仮想テーブルを介して間接化を引き起こします。
2 番目の質問に対する答え: Curiously recurring テンプレート パターンは、通常、プライベート継承を使用して実装されます。「IS-A」関係をモデル化しないため、基本クラスへのポインターを渡しません。
たとえば、
template <class Derived> class Base
{
};
class Derived : Base<Derived>
{
};
Base<Derived>*
a を取り、次に delete を呼び出すコードはありません。したがって、基本クラスへのポインターを介して派生クラスのオブジェクトを削除しようとしないでください。したがって、デストラクタは仮想である必要はありません。
まず、OPの質問に対する答えはかなりよく答えられていると思います-それは確かなNOです。
しかし、それは私が気が狂っているだけなのか、それともコミュニティで何か深刻な問題が起こっているのでしょうか? Base へのポインター/参照を保持することは役に立たない/まれであると多くの人が示唆しているのを見て、少し怖くなりました。上記の一般的な回答のいくつかは、IS-A 関係を CRTP とモデル化していないことを示唆しており、私はそれらの意見に完全に同意しません。
C++ にはインターフェイスのようなものがないことは広く知られています。したがって、テスト可能/モック可能なコードを作成するために、多くの人が ABC を「インターフェース」として使用します。たとえば、関数がvoid MyFunc(Base* ptr)
あり、次のように使用できますMyFunc(ptr_derived)
。これは、MyFunc で仮想関数を呼び出すときに vtable ルックアップを必要とする IS-A 関係をモデル化する従来の方法です。これが IS-A 関係をモデル化するためのパターン 1 です。
パフォーマンスが重要な一部のドメインでは、CRTP を使用して、テスト可能/モック可能な方法で IS-A 関係をモデル化する別の方法 (パターン 2) が存在します。実際、場合によっては、パフォーマンスの大幅な向上 (記事では 600%) が可能です。このリンクを参照してください。したがって、MyFunc は次のようになりますtemplate<typename Derived> void MyFunc(Base<Derived> *ptr)
。MyFunc を使用する場合は、次MyFunc(ptr_derived);
のことを行います。コンパイラは、パラメーターの型 ptr_derived - と最も一致する MyFunc() のコードのコピーを生成しますMyFunc(Base<Derived> *ptr)
。MyFunc の内部では、インターフェイスによって定義された関数が呼び出され、ポインターがコンパイル時に静的にキャストされると想定することができます (リンクの impl() 関数を確認してください)。vtable ルックアップのオーバーヘッドはありません。
さて、誰かが私が非常識なナンセンスを話しているか、または上記の回答が CRTP との IS-A 関係をモデル化するための 2 番目のパターンを単に考慮していないことを教えてもらえますか?