在C++中设计抽象基类(ABC)的良好实践

5
在Java中,我们可以定义不同的接口,然后稍后为具体类实现多个接口。
// Simulate Java Interface in C++
/*
interface IOne {
    void   MethodOne(int i);
    .... more functions
}

interface ITwo {
    double MethodTwo();
    ... more functions
}

class ABC implements IOne, ITwo {
    // implement MethodOne and MethodTwo
}
*/

在C++中,一般情况下,我们应该避免使用多重继承,虽然多重继承在某些情况下确实有其优势。

class ABC {
public:
    virtual void   MethodOne(int /*i*/) = 0 {}
    virtual double MethodTwo() = 0 {}

    virtual ~ABC() = 0 {}

protected:
    ABC() {} // ONLY ABC or subclass can access it
};

问题1:> 基于ABC的设计,我是否需要改进其他方面以使其成为一个优秀的ABC?

问题2:> 一个好的ABC是否应该不包含成员变量,而是将变量保存在子类中?

问题3:> 如我在评论中所述,如果ABC必须包含太多纯函数,有更好的方法吗?


不确定你的实际代码是否类似于第一个示例块,但接口类中的方法声明需要 virtual 关键字;仅在最初声明为 virtual 后,该关键字是可选的。 - ssube
6
这段话的意思是:这个是否可以编译?在纯虚函数中0后面的花括号是一个错误。需要注意的是,这里的“花括号”指的是大括号,也称为curly brackets或者braces。 - Alessandro Pezzato
MSDN关于抽象类的说明:http://msdn.microsoft.com/zh-cn/library/c8whxhf1.aspx - Stefan Birladeanu
1
@AlessandroPezzato:这是正确的。一个纯虚函数不能在类的主体中被定义。它可以在类定义之外被定义。然而,一些编译器(如Visual C++,至少在Visual C++ 2010 SP1)会接受原样的代码。 - James McNellis
@AlessandroPezzato,为什么MSDN没有提供虚析构函数呢? - q0987
Java和C++代码在语义上是不同的,在Java中,您声明了两个接口并在一个类中实现它们,而在C++中,您定义了一个单独的抽象类(接口)。这是有意为之吗?您实际上是什么意思? - David Rodríguez - dribeas
4个回答

11
  1. 除非必要,否则不要为纯虚方法提供实现。
  2. 不要将析构函数设置为纯虚函数。
  3. 不要将构造函数设置为受保护的。您无法创建抽象类的实例。
  4. 最好将构造函数和析构函数的实现隐藏在源文件中,以免污染其他对象文件。
  5. 使您的接口不可复制。

如果这是一个接口,最好不要在其中有任何变量。否则它将成为一个抽象基类而不是接口。

除非您可以使用更少的纯函数,否则太多的纯函数是可以接受的。


Re 3:将构造函数设置为虚函数没有问题,但是……如果该类是抽象的且没有数据成员,为什么要编写构造函数呢?编译器提供的默认构造函数已经可以胜任。 - James Kanze
@JamesKanze:你的意思是应该有一个构造函数而不是“虚拟构造函数”,对吧?但是没错,那里有一个构造函数并没有什么问题,只是需要多打几个字。 - user405725
我是说protected,而不是virtual。将构造函数设置为protected或提供显式构造函数都没有问题。只是像你所说的,需要额外的输入。有人可能会认为这不符合接口的一般模式,因此具有误导性(但我不确定一般模式是否被普遍认可,因此这并不重要)。 - James Kanze

9
在C++中,一般情况下应该避免使用多重继承。和其他语言特性一样,只要适合,就可以使用多重继承。接口通常被认为是多重继承的适当用法(例如,COM)。
ABC的构造函数不需要是受保护的——它不能直接构造,因为它是抽象的。
ABC的析构函数不应该声明为纯虚函数(当然应该声明为虚函数)。你不应该要求派生类实现一个用户声明的构造函数,如果他们不需要一个的话。
接口不应该有任何状态,因此也不应该有任何成员变量,因为接口只定义了如何使用某物,而不是如何实现它。
ABC不应该有太多的成员函数;它应该恰好有所需的数量。如果有太多的成员函数,显然应该删除未使用或不需要的函数,或将接口重构为更具体的几个接口。

1
请注意,如果您接受多重继承(我从未见过不使用它的非平凡应用程序),那么他的所有其他问题都变得无关紧要。 - James Kanze
@q0987 这要看情况。最安全的策略可能是总是从接口虚拟继承,但实际上,这通常是过度设计;例如,实现通常不会被设计为支持继承。另一方面,当一个接口本身继承接口时,通常建议虚拟继承;在我的例子中,我可能应该这样做:尽管没有立即出现基类的多个实例,但一旦实现开始从多个组合接口继承,很容易出现这种情况。 - James Kanze

4
基于ABC的设计,我是否需要改进其他方面以使其成为一个体面的ABC?
你有几个语法错误。由于某种原因,您不允许在类定义中放置纯虚函数的定义;而且无论如何,您几乎肯定不想在ABC中定义它们。因此,这些声明通常会是:
virtual void MethodOne(int /*i*/) = 0;   // ";" not "{}" - just a declaration

没有必要将析构函数设置为纯虚函数,虽然它应该是虚函数(或者在某些情况下是非虚和受保护的-但最安全的方法是使其为虚函数)。

virtual ~ABC() {}  // no "= 0"

不需要受保护的构造函数 - 它是抽象的事实已经防止了除基类外的实例化。

好的抽象基类是否应该不包含成员变量,而应该将变量保留在子类中?

通常是这样的。这使接口和实现之间保持清晰的分离。

如我在评论中所示,如果ABC必须包含过多的纯函数怎么办?有更好的方法吗?

接口应该尽可能复杂,但不要超出需要。只有当一些函数是不必要的时才会有“太多”函数;在这种情况下,摆脱它们。如果接口看起来太复杂,那么它可能试图做的事情比一个多;在这种情况下,您应该能够将其拆分为较小的接口,每个接口都具有单一的目的。


3
首先,为什么我们应该避免在C++中使用多重继承?我从未见过一个大型应用程序不广泛地使用它。从多个接口继承是使用它的好例子。
请注意,Java的“接口”是有缺陷的——一旦您想使用契约式编程,就必须使用抽象类,而它们不允许多重继承。但是,在C++中,这很容易:
class One : boost::noncopyable
{
    virtual void doFunctionOne( int i ) = 0;
public:
    virtual ~One() {}
    void functionOne( int i )
    {
        //  assert pre-conditions...
        doFunctionOne( i );
        //  assert post-conditions...
    }
};

class Two : boost::noncopyable
{
    virtual double doFunctionTwo() = 0;
public:
    virtual ~Two() {}
    double functionTwo()
    {
        //  assert pre-conditions...
        double results = doFunctionTwo();
        //  assert post-conditions...
        return results;
    }
};

class ImplementsOneAndTwo : public One, public Two
{
    virtual void doFunctionOne( int i );
    virtual double doFunctionTwo();
public:
};

或者,您可以拥有一个复合接口:


class OneAndTwo : public One, public Two
{
};

class ImplementsOneAndTwo : public OneAndTwo
{
    virtual void doFunctionOne( int i );
    virtual double doFunctionTwo();
public:
};

并从中继承,选择哪个更有意义。

这是更或多或少的标准用法;在界面中根本不存在任何前置条件或后置条件(通常涉及调用反转)的情况下,虚拟函数可能是公共的,但通常它们将是私有的,以便您可以强制执行前置条件和后置条件。

最后,请注意,在许多情况下(特别是如果类表示值),您将直接实现它,而不是使用界面。与Java不同,您不需要单独的界面来将实现保留在与类定义不同的文件中-这是C ++的默认方式(使用头文件中的类定义,但是实现代码在源文件中)。


OneTwo 类中,doFunctionOnedoFunctionTwo 应该是受保护的或公共的,而不是私有的吗? - q0987
@q0987 当然不是public。我更喜欢private,一些专家也同意这个选择,但protected也有其优点。 - James Kanze
当您定义一个私有虚函数时,哪个子类可以重写它? - q0987
@q0987 全部都受影响。private 只影响谁可以调用它,而不是谁可以覆盖它。 - James Kanze
你对Java接口存在缺陷的看法是错误的。嵌套泛型接口用于契约编程,这些接口是类型安全且非常易读的。 - Roman Smirnov

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