【问题标题】:Good practice to design a ABC(Abstract Base Class) in C++在 C++ 中设计 ABC(抽象基类)的良好实践
【发布时间】:2012-03-06 17:26:50
【问题描述】:

在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> 正如我在 cmets 中指出的,如果 ABC 必须包含太多纯函数怎么办?有没有更好的办法?

【问题讨论】:

  • 不确定你的实际代码是否像第一个示例块,但接口类中的方法声明需要virtual关键字;只有在最初声明 virtual 之后它才是可选的。
  • 这可以编译吗?纯虚函数中0后的大括号是错误
  • @AlessandroPezzato:没错;不能在类的主体中定义纯虚成员函数。它可以在类的定义之外定义。但是,一些编译器(例如 Visual C++,至少从 Visual C++ 2010 SP1 开始)将按原样接受代码。
  • @AlessandroPezzato,为什么 MSDN 没有提供虚拟析构函数?

标签: c++ design-patterns abstract-class


【解决方案1】:
  1. 除非必要,否则不要提供纯虚方法的实现。
  2. 不要将析构函数设为纯虚拟函数。
  3. 不要让您的构造函数受到保护。您不能创建抽象类的实例。
  4. 最好将构造函数和析构函数的实现隐藏在源文件中,以免污染其他目标文件。
  5. 使您的界面不可复制。

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

纯函数太多是可以的,除非你可以用更少的纯函数来做到这一点。

【讨论】:

  • Re 3:将构造函数设为虚拟并没有错,但是......如果类是抽象的,并且没有数据成员,为什么还要编写构造函数呢?提供的默认编译器完成这项工作。
  • @JamesKanze:你的意思是说有一个构造函数而不是一个“虚拟构造函数”,对吧?但是,是的,在那里有一个构造函数并没有错,只是更多的输入。
  • 我的意思是受保护的,而不是虚拟的。使构造函数受保护或提供显式构造函数句号没有任何问题。正如你所说,这只是额外的打字。有人可能会争辩说它不符合接口的一般模式,因此具有误导性(但我不确定一般模式是否被普遍认可以致有所作为)。
【解决方案2】:

在C++中,一般来说,我们应该避免使用多重继承

与任何其他语言功能一样,您应该在适当的地方使用多重继承。接口通常被认为是多重继承的适当使用(例如,参见 COM)。

ABC的构造函数不需要保护——因为它是抽象的,所以不能直接构造。

ABC 析构函数不应被声明为纯虚拟(当然,它应该被声明为虚拟)。如果派生类不需要,则不应要求派生类实现用户声明的构造函数。

接口不应该有任何状态,因此也不应该有任何成员变量,因为接口只定义了如何使用,而不是如何实现。

ABC 不应该有太多成员函数;它应该具有所需的数量。如果太多,你显然应该删除那些不使用或不需要的,或者将接口重构为几个更具体的接口。

【讨论】:

  • 请注意,如果您接受多重继承(而且我从未见过没有广泛使用它的非平凡应用程序),他的所有其他问题都变得无关紧要。
  • @q0987 这取决于。最安全的策略可能总是从接口虚拟继承,但在实践中,这通常是矫枉过正的;例如,实现通常不是为支持继承而设计的。另一方面,当接口本身继承接口时,虚拟继承通常是个好主意。在我的示例中,我可能应该这样做:虽然没有直接的多个基实例,但一旦实现开始从多个复合接口继承,就会很容易发生。
【解决方案3】:

基于 ABC 的设计,我是否应该改进其他的东西才能使它成为一个像样的 ABC?

您有几个语法错误。出于某种原因,不允许将纯虚函数的定义放在类定义中;无论如何,您几乎肯定不想在 ABC 中定义它们。所以声明通常是:

virtual void MethodOne(int /*i*/) = 0;   // ";" not "{}" - just a declaration

将析构函数设为纯并没有任何意义,尽管它应该是虚拟的(或者,在某些情况下,是非虚拟且受保护的 - 但将其设为虚拟是最安全的)。

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

不需要受保护的构造函数 - 它是抽象的事实已经阻止实例化,除非作为基类。

一个好的ABC真的不应该包含成员变量,而是应该将变量保存在子类中吗?

通常,是的。这在接口和实现之间提供了清晰的分离。

正如我在 cmets 中指出的,如果 ABC 必须包含太多纯函数怎么办?有没有更好的办法?

界面应该尽可能复杂,仅此而已。如果某些功能是不必要的,则只有“太多”功能;在这种情况下,摆脱它们。如果界面看起来太复杂,可能是在尝试做不止一件事;在这种情况下,您应该能够将其分解为更小的接口,每个接口都有一个目的。

【讨论】:

    【解决方案4】:

    首先:为什么我们要避免在 C++ 中使用多重继承?我从来没有见过 一个没有广泛使用它的大型应用程序。继承自 多个接口是使用它的一个很好的例子。

    请注意,Java 的 interface 已损坏 — 只要您想使用 通过合同编程,你被困在使用抽象类,并且 他们不允许多重继承。然而,在 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 当然不是public。我更喜欢private,和其他一些专家一样,但也有人支持protected
    • 定义私有虚函数时,哪个子类可以覆盖它?
    • @q0987 所有这些。 private 只影响谁可以调用它,而不影响谁可以覆盖它。
    • 您误认为 Java 接口被破坏了。嵌套泛型接口用于合约编程,它们类型安全且可读性强。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-12-04
    • 2015-04-01
    • 2019-05-29
    • 1970-01-01
    • 2017-08-22
    • 2016-05-24
    • 2011-02-05
    相关资源
    最近更新 更多