如何在C++中模拟接口?

50

因为 C++ 缺乏 Java 和 C# 的 interface 特性,那么在 C++ 类中模拟接口的首选方式是什么?我的猜测是使用抽象类的多重继承。

这种模拟接口的方式对内存开销/性能会有什么影响吗?

是否有任何命名约定用于此类模拟接口,例如 SerializableInterface


13
缺少接口。这似乎是一种消极的措辞。C ++ 并不缺少接口(类就是接口),它只是缺少接口关键字,因为它没有被固定下来。 - Martin York
11
Interface关键字保证不包含代码或数据,因此它们可以轻松地与其他接口交互而不会出现问题。在C++中无法做出这样的保证,你只能希望它们不会做任何引起冲突的事情。Java和C#为了代码的可读性、互操作性和理解性而实现的许多功能,都是针对人们在C++中工作时遇到的问题而得出的结论。 - Bill K
2
可能是如何在C++中声明接口?的重复问题。 - Christophe Weis
9个回答

50

因为C++支持多重继承,而C#和Java不支持,所以你可以创建一系列的抽象类。

至于命名规范,由你决定;然而,我喜欢在类名前加上一个I。

class IStringNotifier
{
public:
  virtual void sendMessage(std::string &strMessage) = 0;
  virtual ~IStringNotifier() { }
};

就性能而言,C#和Java之间的比较并无需担心。基本上,你只需要为函数建立一个查找表或虚函数表(vtable),就像任何具有虚方法的继承一样,这样做会带来一些开销。


9
如果您想使用指向抽象类的指针删除对象,请将抽象类析构函数设置为虚函数。 - Chin Huang
@Jim Huang:同意添加以明确。 - Brian R. Bondy
11
谢谢您的投票。也许如果您根据问题的答案而不是您对编码风格的偏好来判断,会更好? - Brian R. Bondy
3
+1 支持匈牙利命名法。(当然也支持答案 :-) ) - Stephane Rolland

15

没有必要“模拟”任何内容,因为C++没有缺少Java可以使用接口实现的功能。

从C++的角度来看,Java在interfaceclass之间做了一个“人为”的区分。 interface只是一个所有方法都是抽象的class,而且不能包含任何数据成员。

Java之所以会有这样的限制是因为它不允许无限制的多重继承,但它确实允许一个class实现多个接口。

在C++中,class就是class,而interface也是class。通过公共继承实现extends,通过公共继承实现implements

从多个非接口类进行继承可能会导致额外的复杂性,但在某些情况下可能会很有用。如果你限制自己最多只继承一个非接口类和任意数量的完全抽象类,则不会遇到除Java之外的其他困难(当然还有其他C++/Java的不同之处)。

在内存和开销方面,如果您正在重新创建Java风格的类层次结构,则您可能已经在类上支付了虚函数的成本。考虑到您正在使用不同的运行时环境,因此在不同继承模型的开销方面并没有根本性的差异。


8
“在内存开销/性能方面会有什么影响?”
通常情况下并不影响,除非完全使用虚函数调用,尽管标准在性能方面没有做出明确保证。对于内存开销,“空基类”优化允许编译器显式布局结构,使得添加没有数据成员的基类不会增加对象的大小。我认为你不太可能遇到不执行此操作的编译器,但我也可能错了。
向类添加第一个虚成员函数通常会将对象与没有虚成员函数的对象相比增加一个指针的大小。添加更多虚成员函数不会再次增加大小。添加虚基类可能会带来进一步的差异,但对于你所讨论的内容并不需要。
添加具有虚成员函数的多个基类可能意味着实际上只能获得一次空基类优化,因为在典型实现中,对象将需要多个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

7

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个静态表和在尚未具有虚函数的派生类中,一个指向静态表的指针。


2
我认为你不能拥有虚构造函数。不过,你可以拥有虚析构函数。 - jkeys
2
我认为析构函数没有必要是纯虚函数,普通的虚函数就足够了。此外,声明析构函数是不够的,它必须被定义。在纯虚析构函数的情况下,它必须在类定义之外被定义,像这样: ISerializable :: ~ ISerializable(){ } 因为C ++语法不允许同时使用纯虚指示符和类内成员函数定义。 - robson3.14

3

在C++中,我们可以比Java等语言的纯粹无行为接口更进一步。我们可以使用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
{
    ...
};

2
C++ 中的接口也可以静态地出现,通过记录模板类型参数的要求。
模板模式匹配语法,因此您不必事先指定特定类型实现特定接口,只要它具有正确的成员即可。这与 Java 的 <? extends Interface> 或 C# 的 where T : IInterface 约束风格形成对比,后者要求替换类型知道(IInterface
其中一个很好的例子是 Iterator 家族,它们是由指针等实现的。

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没有必需的虚析构函数。


我认为这更多是使用COM/DCOM方式来进行C++编程。 - Stephane Rolland
仅仅因为我只在使用COM接口时看到过它的运用,所以这是唯一的地方。;-) - Stephane Rolland

1

如果您不使用虚拟继承,那么开销应该不会比至少有一个虚函数的常规继承更糟糕。每个抽象类继承都会向每个对象添加一个指针。

但是,如果您像空基类优化一样做一些事情,就可以将其最小化:

struct A
{
    void func1() = 0;
};
struct B: A { void func2() = 0; };
struct C: B { int i; };

C 的大小将为两个字。


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的引用,但编译器会调整传递的地址以匹配正确的基类类型。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接