C++にはJavaおよびC#のinterface
機能がないため、C++クラスのインターフェイスをシミュレートするための好ましい方法は何ですか?抽象クラスの多重継承だと思います。メモリのオーバーヘッド/パフォーマンスの観点からの影響?SerializableInterface
など、そのようなシミュレートされたインターフェイスの命名規則はありますか?
C++にはC#やJavaとは異なり、多重継承があるため、一連の抽象クラスを作成できます。
慣例に関しては、それはあなた次第です。ただし、クラス名の前にIを付けるのが好きです。
class IStringNotifier
{
public:
virtual void sendMessage(std::string &strMessage) = 0;
virtual ~IStringNotifier() { }
};
C#とJavaの比較では、パフォーマンスについて心配する必要はありません。基本的には、仮想メソッドを使用したあらゆる種類の継承と同じように、関数またはvtableのルックアップテーブルを用意するというオーバーヘッドが発生します。
Javaがインターフェイスで実行できることは、C++にないわけではないので、実際には何も「シミュレート」する必要はありません。
C++ポインターの観点から、Javaは、interface
とclass
を「人工的に」区別します。interface
は単なるclass
すべてのメソッドは抽象的であり、データメンバーを含めることはできません。
Javaは、制約のない多重継承を許可しないため、この制限を設けていますが、class
からimplement
の複数のインターフェースは許可しています。
C++では、class
はclass
であり、interface
はclass
です。 extends
はパブリック継承によって実現され、implements
もパブリック継承によって実現されます。
複数の非インターフェースクラスから継承すると、さらに複雑になる可能性がありますが、状況によっては役立つ場合があります。最大で1つの非インターフェイスクラスと任意の数の完全に抽象化されたクラスからのクラスのみを継承するように制限する場合、Java(Java $ ===(その他のC++/Java違いを除く)。
メモリとオーバーヘッドのコストに関して、Javaスタイルのクラス階層を再作成する場合は、いずれにせよ、クラスの仮想関数のコストをすでに支払っている可能性があります。いずれにせよ、異なるランタイム環境では、異なる継承モデルのコストに関して、2つの間のオーバーヘッドに基本的な違いはありません。
「メモリのオーバーヘッド/パフォーマンスに関してどのような影響がありますか?」
通常、仮想通話を使用する場合を除いて、まったく何もありませんが、パフォーマンスの点で標準によって保証されるものはほとんどありません。
メモリオーバーヘッドでは、「空の基本クラス」最適化により、コンパイラは、データメンバーを持たない基本クラスを追加してもオブジェクトのサイズが増加しないように、構造をレイアウトできます。これを行わないコンパイラを扱う必要はないと思いますが、私は間違っている可能性があります。
クラスに最初の仮想メンバー関数を追加すると、通常、オブジェクトに仮想メンバー関数がない場合と比較して、ポインターのサイズだけオブジェクトが増加します。さらに仮想メンバー関数を追加しても、追加の違いはありません。仮想基本クラスを追加すると、さらに違いが生じる可能性がありますが、話している内容にはそれは必要ありません。
仮想メンバー関数を使用して複数の基本クラスを追加すると、通常の実装ではオブジェクトに複数のvtableポインターが必要になるため、実際には空の基本クラスの最適化を1回だけ取得することになります。したがって、各クラスに複数のインターフェースが必要な場合は、オブジェクトのサイズを増やす可能性があります。
パフォーマンス上、仮想関数呼び出しは非仮想関数呼び出しよりもわずかにオーバーヘッドが大きく、さらに重要なことに、一般的に(常に?)インライン化されないと想定できます。空の基本クラスを追加しても、通常、構築または破棄にコードは追加されません。これは、空の基本コンストラクタとデストラクタを派生クラスのコンストラクタ/デストラクタコードにインライン化できるためです。
明示的なインターフェイスが必要な場合に仮想関数を回避するために使用できるトリックがありますが、動的ポリモーフィズムは必要ありません。ただし、Javaをエミュレートしようとしている場合は、そうではないと思います。
コード例:
#include <iostream>
// A is an interface
struct A {
virtual ~A() {};
virtual int a(int) = 0;
};
// B is an interface
struct B {
virtual ~B() {};
virtual int b(int) = 0;
};
// C has no interfaces, but does have a virtual member function
struct C {
~C() {}
int c;
virtual int getc(int) { return c; }
};
// D has one interface
struct D : public A {
~D() {}
int d;
int a(int) { return d; }
};
// E has two interfaces
struct E : public A, public B{
~E() {}
int e;
int a(int) { return e; }
int b(int) { return e; }
};
int main() {
E e; D d; C c;
std::cout << "A : " << sizeof(A) << "\n";
std::cout << "B : " << sizeof(B) << "\n";
std::cout << "C : " << sizeof(C) << "\n";
std::cout << "D : " << sizeof(D) << "\n";
std::cout << "E : " << sizeof(E) << "\n";
}
出力(32ビットプラットフォームのGCC):
A : 4
B : 4
C : 8
D : 8
E : 12
C++のインターフェイスは、純粋仮想関数のみを持つクラスです。例えば。 :
class ISerializable
{
public:
virtual ~ISerializable() = 0;
virtual void serialize( stream& target ) = 0;
};
これはシミュレートされたインターフェースではなく、Javaのようなインターフェースですが、欠点はありません。
例えば。悪影響を与えることなくメソッドとメンバーを追加できます:
class ISerializable
{
public:
virtual ~ISerializable() = 0;
virtual void serialize( stream& target ) = 0;
protected:
void serialize_atomic( int i, stream& t );
bool serialized;
};
命名規則について... C++言語で定義されている実際の命名規則はありません。したがって、ご使用の環境で1つを選択してください。
オーバーヘッドは1つの静的テーブルであり、仮想関数をまだ持っていない派生クラスでは、静的テーブルへのポインターです。
C++では、Java&co。)の単純な動作のないインターフェイスよりもさらに進んでいます。NVIパターンを使用して明示的なコントラクトを追加できます(契約による設計のように) 。
struct Contract1 : noncopyable
{
virtual ~Contract1();
Res f(Param p) {
assert(f_precondition(p) && "C1::f precondition failed");
const Res r = do_f(p);
assert(f_postcondition(p,r) && "C1::f postcondition failed");
return r;
}
private:
virtual Res do_f(Param p) = 0;
};
struct Concrete : virtual Contract1, virtual Contract2
{
...
};
C++のインターフェイスは、テンプレートタイプパラメータの要件を文書化することにより、静的に発生することもあります。
テンプレートのパターンは構文に一致するため、適切なメンバーがある限り、特定のタイプが特定のインターフェイスを実装することを事前に指定する必要はありません 。これは、Javaの<? extends Interface>
またはC#のwhere T : IInterface
スタイル制約とは対照的であり、置換型は(I
)Interface
について知る必要があります。
この良い例は Iterator ファミリーで、これはとりわけポインターによって実装されます。
仮想継承を使用しない場合、オーバーヘッドは、少なくとも1つの仮想関数を使用した通常の継承よりも悪くないはずです。から継承された各抽象クラスは、各オブジェクトへのポインタを追加します。
ただし、Empty Base Class Optimizationのようなことを行う場合は、それを最小限に抑えることができます。
struct A { void func1()= 0; }; struct B:A { void func2()= 0; }; struct C:B { int i; };
Cのサイズは2ワードになります。
ちなみにMSVC2008には __ interface キーワードがあります。
A Visual C++ interface can be defined as follows:
- Can inherit from zero or more base
interfaces.
- Cannot inherit from a base class.
- Can only contain public, pure virtual
methods.
- Cannot contain constructors,
destructors, or operators.
- Cannot contain static methods.
- Cannot contain data members;
properties are allowed.
この機能はMicrosoft固有です。注意: __ interface には、インターフェイスポインタでオブジェクトを削除する場合に必要な仮想デストラクタはありません。
あなたが求めている方法でインターフェースを実装する良い方法はありません。完全に抽象的なISerializable基本クラスなどのアプローチの問題は、C++が多重継承を実装する方法にあります。次のことを考慮してください。
class Base
{
};
class ISerializable
{
public:
virtual string toSerial() = 0;
virtual void fromSerial(const string& s) = 0;
};
class Subclass : public Base, public ISerializable
{
};
void someFunc(fstream& out, const ISerializable& o)
{
out << o.toSerial();
}
明らかに、関数toSerial()は、Baseクラスから継承するメンバーを含むSubclassのすべてのメンバーをシリアル化することを目的としています。問題は、ISerializableからBaseへのパスがないことです。以下を実行すると、これをグラフィカルに確認できます。
void fn(Base& b)
{
cout << (void*)&b << endl;
}
void fn(ISerializable& i)
{
cout << (void*)&i << endl;
}
void someFunc(Subclass& s)
{
fn(s);
fn(s);
}
最初の呼び出しで出力された値は、2番目の呼び出しで出力された値と同じではありません。どちらの場合もsへの参照が渡されますが、コンパイラは渡されたアドレスを適切な基本クラスタイプに一致するように調整します。