ご存じのとおり、一部の言語にはインターフェイスの概念があります。これはJavaです。
public interface Testable {
void test();
}
C++(またはC++ 11)でこれを最もコンパクトな方法で、コードノイズの少ない方法で実現するにはどうすればよいですか?個別の定義を必要としないソリューション(ヘッダーで十分)をいただければ幸いです。これは非常に単純なアプローチであり、バグが見つかったとしても;-)
class Testable {
public:
virtual void test() = 0;
protected:
Testable();
Testable(const Testable& that);
Testable& operator= (const Testable& that);
virtual ~Testable();
}
これはほんの始まりに過ぎません。そして、私が望んでいるのはすでに長いです。それを改善するには?たぶんこれのために作られたstd名前空間のどこかに基本クラスがあるのでしょうか?
どうですか:
class Testable
{
public:
virtual ~Testable() { }
virtual void test() = 0;
}
C++では、これは子クラスのコピー可能性に影響しません。これは、子がtest
(インターフェイスにまさに必要なもの)を実装する必要があるということだけです。このクラスをインスタンス化することはできません。そのため、暗黙的なコンストラクターを親インターフェース型として直接呼び出すことはできないため、暗黙的なコンストラクターについて心配する必要はありません。
子クラスにデストラクタを実装することを強制したい場合は、デストラクタを純粋にすることもできます(ただし、インターフェイスに実装する必要があります)。
また、ポリモーフィックな破壊が不要な場合は、代わりにデストラクタを非仮想的に保護することを選択できます。
動的(実行時)ポリモーフィズムの場合、Non-Virtual-Interface(NVI)イディオムを使用することをお勧めします。このパターンは、インターフェイスを非仮想およびパブリック、デストラクタを仮想およびパブリック、実装を純粋な仮想およびプライベートに保ちます
class DynamicInterface
{
public:
// non-virtual interface
void fun() { do_fun(); } // equivalent to "this->do_fun()"
// enable deletion of a Derived* through a Base*
virtual ~DynamicInterface() = default;
private:
// pure virtual implementation
virtual void do_fun() = 0;
};
class DynamicImplementation
:
public DynamicInterface
{
private:
virtual void do_fun() { /* implementation here */ }
};
動的なポリモーフィズムの素晴らしい点は、実行時に、インターフェイスの基本クラスへのポインターまたは参照が予想される任意の派生クラスを渡すことができることです。ランタイムシステムは、this
ポインターを静的な基本型から動的な派生型に自動的にダウンキャストし、対応する実装を呼び出します(通常、仮想関数へのポインターを持つテーブルを介して発生します)。
静的(コンパイル時のポリモーフィズム)の場合、Curiously Recurring Template Pattern(CRTP)を使用することをお勧めします。動的なポリモーフィズムのベースから派生への自動ダウンキャストは、static_cast
。この静的キャストは、各静的インターフェイスの派生元のヘルパークラスで定義できます。
template<typename Derived>
class enable_down_cast
{
private:
typedef enable_down_cast Base;
public:
Derived const* self() const
{
// casting "down" the inheritance hierarchy
return static_cast<Derived const*>(this);
}
Derived* self()
{
return static_cast<Derived*>(this);
}
protected:
// disable deletion of Derived* through Base*
// enable deletion of Base* through Derived*
~enable_down_cast() = default; // C++11 only, use ~enable_down_cast() {} in C++98
};
次に、次のような静的インターフェイスを定義します。
template<typename Impl>
class StaticInterface
:
// enable static polymorphism
public enable_down_cast< Impl >
{
private:
// dependent name now in scope
using enable_down_cast< Impl >::self;
public:
// interface
void fun() { self()->do_fun(); }
protected:
// disable deletion of Derived* through Base*
// enable deletion of Base* through Derived*
~StaticInterface() = default; // C++11 only, use ~IFooInterface() {} in C++98/03
};
そして最後に、インターフェイスから派生する実装を作成しますそれ自体がパラメーターとして
class StaticImplementation
:
public StaticInterface< StaticImplementation >
{
private:
// implementation
friend class StaticInterface< StaticImplementation > ;
void do_fun() { /* your implementation here */ }
};
これにより、同じインターフェイスの複数の実装を使用できますが、コンパイル時に呼び出している実装を知る必要があります。
つまり、どのフォームをいつ使用するか両方のフォームを使用すると、共通のインターフェイスを再利用して、インターフェイスクラス内に事前/事後条件テストを挿入できます。動的なポリモーフィズムの利点は、実行時の柔軟性があることですが、仮想関数呼び出し(通常はインライン化の機会がほとんどない関数ポインターを介した呼び出し)で支払います。静的多態性はそのミラーです。仮想関数呼び出しのオーバーヘッドはありませんが、不利な点は、より多くの定型コードが必要であり、コンパイル時に何を呼び出しているかを知る必要があることです。基本的に、効率と柔軟性のトレードオフです。
注:コンパイル時のポリフォリズムでは、テンプレートパラメータを使用することもできます。 CRTPイディオムによる静的インターフェイスと通常のテンプレートパラメーターの違いは、CRTPタイプのインターフェイスは明示的(メンバー関数に基づく)であり、テンプレートインターフェイスは暗黙的(有効な式に基づく)であるということです。
Scott Meyers(Effective Modern C++)によると:インターフェース(またはポリモーフィックな基本クラス)を宣言する場合、ベースを通じてアクセスされる派生クラスオブジェクトのdelete
またはtypeid
などの操作の適切な結果のために、仮想デストラクターが必要ですクラスポインターまたは参照。
virtual ~Testable() = default;
ただし、ユーザーが宣言したデストラクタは移動操作の生成を抑制するため、移動操作をサポートするには以下を追加する必要があります。
Testable(Testable&&) = default;
Testable& operator=(Testable&&) = default;
移動操作を宣言すると、コピー操作が無効になり、次も必要になります。
Testable(const Testable&) = default;
Testable& operator=(const Testable&) = default;
最終結果は次のとおりです。
class Testable
{
public:
virtual ~Testable() = default; // make dtor virtual
Testable(Testable&&) = default; // support moving
Testable& operator=(Testable&&) = default;
Testable(const Testable&) = default; // support copying
Testable& operator=(const Testable&) = default;
virtual void test() = 0;
};
ここで別の興味深い記事: C++のゼロの規則
Word class
をstruct
に置き換えることにより、すべてのメソッドはデフォルトでパブリックになり、行を保存できます。
とにかく純粋な仮想メソッドでクラスをインスタンス化することはできないため、コンストラクターを保護する必要はありません。これはコピーコンストラクタにも当てはまります。コンパイラによって生成されたデフォルトコンストラクターは、データメンバーがないため空になり、派生クラスには完全に十分です。
コンパイラによって生成された演算子は確かに間違ったことをするので、=
演算子を心配するのは正しいことです。あるインターフェイスオブジェクトを別のインターフェイスオブジェクトにコピーしても意味がないため、実際には誰も心配していません。よくある間違いではありません。
継承可能なクラスのデストラクタは、always publicおよびvirtual、またはprotectedおよびnon-virtualである必要があります。この場合、パブリックおよびバーチャルを好みます。
最終的な結果は、Java同等のものよりも1行だけ長くなります。
struct Testable {
virtual void test() = 0;
virtual ~Testable();
};
ポインター、ハンドル、および/またはクラスのすべてのデータメンバーがクリーンアップを管理する独自のデストラクタを管理していない場合、「3つのルール」は不要であることに注意してください。また、仮想基本クラスの場合、基本クラスを直接インスタンス化することはできないため、データメンバーのないインターフェイスを定義するだけであれば、コンストラクターを宣言する必要はありません...コンパイラーデフォルトは問題ありません。インターフェイス型のポインターでdelete
を呼び出すことを計画している場合、保持する必要がある唯一のアイテムは仮想デストラクタです。したがって、実際には、インターフェイスは次のように簡単にできます。
class Testable
{
public:
virtual void test() = 0;
virtual ~Testable();
}