由于C ++缺少Java和C#的interface
特性,在C ++类中模拟接口的首选方法是什么?我的猜测是抽象类的多重继承。
在内存开销/性能方面有什么影响?
是否存在此类模拟接口的命名约定,例如SerializableInterface
?
答案 0 :(得分:36)
由于C ++具有多个继承而不像C#和Java,是的,你可以制作一系列抽象类。
至于惯例,由你决定;但是,我喜欢在类名之前加上I。
class IStringNotifier
{
public:
virtual void sendMessage(std::string &strMessage) = 0;
virtual ~IStringNotifier() { }
};
在C#和Java之间的比较方面,性能无需担心。基本上你只需要为你的函数或vtable创建一个查找表,就像使用虚方法给出的任何类型的继承一样。
答案 1 :(得分:11)
实际上没有必要'模拟'任何东西,因为不是C ++缺少Java可以用接口做的任何事情。
从C ++指针来看,Java在interface
和class
之间进行了“人为”的区分。 interface
只是class
,其所有方法都是抽象的,不能包含任何数据成员。
Java提出了这个限制,因为它不允许无约束的多重继承,但它确实允许class
到implement
多个接口。
在C ++中,class
是class
而interface
是class
。 extends
是通过公共继承实现的,implements
也是通过公共继承实现的。
从多个非接口类继承可能会导致额外的复杂性,但在某些情况下可能很有用。如果你自己只限于从最多一个非接口类和任何数量的完全抽象类继承类,那么你将不会遇到任何其他困难,而不是在Java中(除了其他C ++ / Java差异)。 / p>
就内存和开销成本而言,如果要重新创建Java样式类层次结构,那么在任何情况下,您可能已经在类上支付了虚函数成本。鉴于您无论如何都在使用不同的运行时环境,在不同的继承模型的成本方面,两者之间的开销不会有任何根本的差异。
答案 2 :(得分:7)
“在内存开销/性能方面有什么影响?”
除了使用虚拟呼叫之外通常都没有,尽管标准在性能方面没有什么保证。
在内存开销上,“空基类”优化显式允许编译器布局结构,以便添加没有数据成员的基类不会增加对象的大小。我认为你不太可能要处理不能做到这一点的编译器,但我可能错了。
将第一个虚拟成员函数添加到类中通常会增加指针大小的对象,与没有虚拟成员函数的情况相比。添加更多虚拟成员函数没有任何其他区别。添加虚拟基类可能会产生进一步的不同,但您不需要那些正在谈论的内容。
使用虚拟成员函数添加多个基类可能意味着实际上只获得一次空基类优化,因为在典型的实现中,对象需要多个vtable指针。因此,如果每个类需要多个接口,则可能会增加对象的大小。
在性能方面,虚拟函数调用比非虚函数调用具有更多的开销,更重要的是,您可以假设它通常(总是?)不会内联。添加空基类通常不会为构造或销毁添加任何代码,因为空基构造函数和析构函数可以内联到派生类构造函数/析构函数代码中。
如果您想要显式接口,可以使用一些技巧来避免虚函数,但是您不需要动态多态。但是,如果您正在尝试模拟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
答案 3 :(得分:5)
C ++中的接口是只具有纯虚函数的类。例如。 :
class ISerializable
{
public:
virtual ~ISerializable() = 0;
virtual void serialize( stream& target ) = 0;
};
这不是一个模拟界面,它是一个类似于Java的界面,但没有缺点。
E.g。您可以添加方法和成员而不会产生负面后果:
class ISerializable
{
public:
virtual ~ISerializable() = 0;
virtual void serialize( stream& target ) = 0;
protected:
void serialize_atomic( int i, stream& t );
bool serialized;
};
对于命名约定......在C ++语言中没有定义真正的命名约定。所以选择你环境中的那个。
开销是1个静态表,在没有虚函数的派生类中,是指向静态表的指针。
答案 4 :(得分:3)
在C ++中,我们可以比Java&amp;的简单无行为接口更进一步。合。 我们可以使用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
{
...
};
答案 5 :(得分:1)
如果不使用虚拟继承,则开销应该不比具有至少一个虚函数的常规继承差。从中继承的每个抽象类都将添加一个指向每个对象的指针。
但是,如果您执行类似空基类优化的操作,则可以将其最小化:
struct A { void func1() = 0; }; struct B: A { void func2() = 0; }; struct C: B { int i; };
C的大小将是两个单词。
答案 6 :(得分:1)
顺便说一句,MSVC 2008有__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没有必需的虚析构函数。
答案 7 :(得分:1)
通过记录对模板类型参数的要求,C ++中的接口也可以静态发生。
模板模式匹配语法,因此只要具有正确的成员,就不必指定 upfront 特定类型实现特定接口。这与Java的<? extends Interface>
或C#的where T : IInterface
样式约束相反,后者要求替换的类型知道(I
)Interface
。
Iterator系列就是一个很好的例子,它是由指针实现的。
答案 8 :(得分:0)
没有好方法以您要求的方式实现界面。诸如完全抽象的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()来序列化Subclass的所有成员,包括它从Base类继承的成员。问题是没有从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);
}
第一次调用输出的值与第二次调用输出的值不同。即使在两种情况下都传递了对s的引用,编译器也会调整传递的地址以匹配正确的基类类型。