How can I simulate interfaces in C++?
由于C++缺乏Java和C语言的EDOCX1 0特征,在C++类中模拟接口的首选方法是什么?我的猜测是抽象类的多重继承。内存开销/性能方面的影响是什么?这些模拟接口有没有命名约定,比如
因为C++和C语言和Java有多种继承关系,是的,你可以制作一系列抽象类。
至于惯例,这取决于您;不过,我喜欢在类名前面加一个i。
1 2 3 4 5 6 | class IStringNotifier { public: virtual void sendMessage(std::string &strMessage) = 0; virtual ~IStringNotifier() { } }; |
从C语言和Java语言的比较来看,性能无需担心。基本上,您只需要为函数或vtable设置一个查找表的开销,就像使用虚拟方法进行继承一样。
实际上没有必要"模拟"任何东西,因为不是C++忽略了Java可以用接口做的任何事情。
从C++的指针来看,Java在EDCOX1×0和EDCOX1×1之间建立了一个"人工"的磁盘空间。
Java在不允许无约束多重继承的情况下实现了这种限制,但是它确实允许EDCOX1对1的EDCOX1(5)多个接口。
在C++中,EDOCX1×1是EDOCX1×1,EDCOX1×0是EDCOX1×1。
从多个非接口类继承可能会导致额外的复杂性,但在某些情况下是有用的。如果您只限于从最多一个非接口类和任意数量的完全抽象类继承类,那么您将不会遇到比Java中其他任何其他困难(其他C++ + Java差异除外)。
在内存和开销方面,如果你正在重新创建一个Java风格的类层次结构,那么在任何情况下,你可能已经在你的类上支付了虚拟函数开销。考虑到您使用的是不同的运行时环境,就不同继承模型的成本而言,两者之间的开销不会有任何根本区别。
"内存开销/性能方面的影响是什么?"
通常,除了那些使用虚拟调用的调用之外,没有其他调用,尽管标准在性能方面没有太多的保证。
在内存开销上,"空基类"优化显式地允许编译器布局结构,以便添加没有数据成员的基类不会增加对象的大小。我认为您不太可能需要处理不执行此操作的编译器,但我可能是错的。
与没有虚拟成员函数的对象相比,向类中添加第一个虚拟成员函数通常会增加指针大小的对象。添加更多的虚拟成员函数不会产生额外的差异。添加虚拟基类可能会有更大的不同,但对于您所说的内容,您不需要这样做。
使用虚拟成员函数添加多个基类可能意味着实际上您只获得一次空的基类优化,因为在典型的实现中,对象需要多个vtable指针。因此,如果在每个类上需要多个接口,则可能会增加对象的大小。
在性能方面,虚函数调用比非虚函数调用的开销要稍微大一点,更重要的是,您可以假定它通常(总是?)。不会被内联。添加空的基类通常不会向构造或销毁添加任何代码,因为空的基本构造函数和析构函数可以内联到派生类的构造函数/析构函数代码中。
如果需要显式接口,可以使用一些技巧来避免虚拟函数,但不需要动态多态性。但是,如果你试图模仿Java,那么我认为情况并非如此。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | #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) <<" "; std::cout <<"B :" << sizeof(B) <<" "; std::cout <<"C :" << sizeof(C) <<" "; std::cout <<"D :" << sizeof(D) <<" "; std::cout <<"E :" << sizeof(E) <<" "; } |
输出(32位平台上的GCC):
1 2 3 4 5 | A : 4 B : 4 C : 8 D : 8 E : 12 |
C++中的接口是纯虚拟函数的类。例如:
1 2 3 4 5 6 | class ISerializable { public: virtual ~ISerializable() = 0; virtual void serialize( stream& target ) = 0; }; |
这不是一个模拟接口,它是一个接口,就像Java中的接口一样,但是没有缺点。
例如,您可以添加方法和成员,而不会产生负面影响:
1 2 3 4 5 6 7 8 9 | class ISerializable { public: virtual ~ISerializable() = 0; virtual void serialize( stream& target ) = 0; protected: void serialize_atomic( int i, stream& t ); bool serialized; }; |
到命名约定…在C++语言中没有真正的命名约定。所以在你的环境中选择一个。
开销是1个静态表,在还没有虚拟函数的派生类中,是指向静态表的指针。
在C++中,我们可以比Java公司的纯接口更少。我们可以使用nvi模式添加显式契约(如按契约设计)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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的EDCOX1、12或C的EDOCX1×13风格约束相反,这要求替换类型知道EDCOX1(14),EDOCX1×0。
其中一个很好的例子是迭代器家族,它是由指针实现的。
顺便说一下,MSVC 2008有uu interface关键字。
1 2 3 4 5 6 7 8 9 10 11 12 | 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。注意:如果您通过接口指针删除对象,则_uu接口没有所需的虚拟析构函数。
如果不使用虚拟继承,则开销不应比具有至少一个虚拟函数的常规继承更糟糕。继承自的每个抽象类都将向每个对象添加一个指针。
但是,如果您执行类似于空基类优化的操作,则可以将其最小化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | struct A { void func1() = 0; }; struct B: A { void func2() = 0; }; struct C: B { int i; }; |
C的大小是两个单词。
没有好的方法可以像您所要求的那样实现接口。一个方法的问题,如完全抽象的可分级基类,在于C++实现多重继承的方式。考虑以下事项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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()的目的是序列化子类的所有成员,包括它从基类继承的成员。问题是没有从ISerializable到Base的路径。如果执行以下操作,您可以以图形方式看到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void fn(Base& b) { cout << (void*)&b << endl; } void fn(ISerializable& i) { cout << (void*)&i << endl; } void someFunc(Subclass& s) { fn(s); fn(s); } |
第一个调用输出的值与第二个调用输出的值不同。即使在这两种情况下都传递了对s的引用,编译器也会调整传递的地址以匹配正确的基类类型。