50

众所周知,有些语言有接口的概念。这是Java:

public interface Testable {
  void test();
}

如何以最紧凑的方式在 C++(或 C++11)中实现这一点,并且代码噪音很小?我很欣赏不需要单独定义的解决方案(让标题就足够了)。这是一种非常简单的方法,即使我也觉得有问题 ;-)

class Testable {
public:
  virtual void test() = 0;
protected:
  Testable();
  Testable(const Testable& that);
  Testable& operator= (const Testable& that);
  virtual ~Testable();
}

这只是开始……而且我想要的时间已经更长了。如何改进它?也许在 std 命名空间中的某个地方有一个基类就是为此而生的?

4

5 回答 5

46

对于动态(运行时)多态性,我建议使用非虚拟接口(NVI) 习惯用法。这种模式保持接口非虚拟和公共,析构函数虚拟和公共,以及实现纯虚拟和私有

class DynamicInterface
{
public:
    // non-virtual interface
    void fun() { do_fun(); } // equivalent to "this->do_fun()"

    // enable deletion of a Derived* through a Base*
    virtual ~DynamicInterface() = default;    
private:
    // pure virtual implementation
    virtual void do_fun() = 0; 
};

class DynamicImplementation
:
    public DynamicInterface
{
private:
    virtual void do_fun() { /* implementation here */ }
};

动态多态性的好处是您可以在运行时传递任何派生类,其中需要指向接口基类的指针或引用。this运行时系统会自动将指针从其静态基类型向下转换为其动态派生类型并调用相应的实现(通常通过带有指向虚函数的指针的表发生)。

对于静态(编译时多态性),我建议使用Curiously Recurring Template Pattern (CRTP)。这涉及更多,因为必须使用static_cast. 此静态转换可以在每个静态接口派生自的帮助器类中定义

template<typename Derived>
class enable_down_cast
{
private:  
        typedef enable_down_cast Base;    
public:
        Derived const* self() const
        {
                // casting "down" the inheritance hierarchy
                return static_cast<Derived const*>(this);
        }

        Derived* self()
        {
                return static_cast<Derived*>(this);
        }    
protected:
        // disable deletion of Derived* through Base*
        // enable deletion of Base* through Derived*
        ~enable_down_cast() = default; // C++11 only, use ~enable_down_cast() {} in C++98
};

然后你定义一个像这样的静态接口:

template<typename Impl>
class StaticInterface
:
    // enable static polymorphism
    public enable_down_cast< Impl >
{
private:
    // dependent name now in scope
    using enable_down_cast< Impl >::self;    
public:
    // interface
    void fun() { self()->do_fun(); }    
protected:
    // disable deletion of Derived* through Base*
    // enable deletion of Base* through Derived*
    ~StaticInterface() = default; // C++11 only, use ~IFooInterface() {} in C++98/03
};

最后,您创建了一个从接口派生的实现,并将其自身作为参数

class StaticImplementation
:
    public StaticInterface< StaticImplementation > 
{
private:
    // implementation
    friend class StaticInterface< StaticImplementation > ;
    void do_fun() { /* your implementation here */ }
};

这仍然允许您拥有同一接口的多个实现,但您需要在编译时知道您正在调用哪个实现。

那么什么时候使用哪种形式呢?这两种形式都可以让您重用通用接口并在接口类中注入前置/后置条件测试。动态多态性的优点是您具有运行时灵活性,但您在虚函数调用中为此付出代价(通常通过函数指针进行调用,几乎没有内联的机会)。静态多态是其镜像:没有虚函数调用开销,但缺点是您需要更多样板代码,并且您需要知道在编译时调用的是什么。基本上是效率/灵活性的权衡。

注意:对于编译时多态,您也可以使用模板参数。通过 CRTP 成语的静态接口与普通模板参数的区别在于 CRTP 类型的接口是显式的(基于成员函数),而模板接口是隐式的(基于有效的表达式)

于 2013-01-14T18:40:21.780 回答
41

关于什么:

class Testable
{
public:
    virtual ~Testable() { }
    virtual void test() = 0;
}

在 C++ 中,这对子类的可复制性没有影响。所有这一切都是孩子必须实现test(这正是您想要的接口)。您无法实例化此类,因此您不必担心任何隐式构造函数,因为它们不能直接作为父接口类型调用。

如果您希望强制子类实现析构函数,您也可以将其设为纯(但您仍然必须在接口中实现它)。

另请注意,如果您不需要多态破坏,则可以选择使您的析构函数受保护非虚拟。

于 2013-01-14T17:41:50.057 回答
24

根据 Scott Meyers(有效的现代 C++):当声明接口(或多态基类)时,您需要虚拟析构函数,以获得正确的操作结果,例如通过基类指针deletetypeid引用访问的派生类对象。

virtual ~Testable() = default;

但是,用户声明的析构函数会抑制移动操作的生​​成,因此要支持移动操作,您需要添加:

Testable(Testable&&) = default; 
Testable& operator=(Testable&&) = default;

声明移动操作会禁用复制操作,您还需要:

Testable(const Testable&) = default;
Testable& operator=(const Testable&) = default;

最后的结果是:

class Testable 
{
public:
    virtual ~Testable() = default; // make dtor virtual
    Testable(Testable&&) = default;  // support moving
    Testable& operator=(Testable&&) = default;
    Testable(const Testable&) = default; // support copying
    Testable& operator=(const Testable&) = default;

    virtual void test() = 0;

};

另一篇有趣的文章:C++ 中的零规则

于 2015-06-17T08:11:50.360 回答
12

通过将单词替换为classstruct默认情况下所有方法都是公共的,您可以节省一行。

没有必要让构造函数受到保护,因为无论如何你都不能用纯虚方法实例化一个类。这也适用于复制构造函数。编译器生成的默认构造函数将是空的,因为您没有任何数据成员,并且对于您的派生类来说完全足够了。

您担心=运算符是正确的,因为编译器生成的运算符肯定会做错事。实际上,没有人会担心它,因为将一个接口对象复制到另一个是没有意义的。这不是经常发生的错误。

可继承类的析构函数应该始终是公共的和虚拟的,或者受保护的和非虚拟的。在这种情况下,我更喜欢公共和虚拟的。

最终结果仅比 Java 等效项长一行:

struct Testable {
    virtual void test() = 0;
    virtual ~Testable();
};
于 2013-01-14T19:37:11.750 回答
7

请记住,如果您不管理指针、句柄和/或类的所有数据成员都有自己的析构函数来管理任何清理,那么“三规则”是不必要的。同样在虚拟基类的情况下,因为基类永远不能直接实例化,所以如果您想要做的只是定义一个没有数据成员的接口,则无需声明构造函数......编译器默认值就好了。delete如果您计划调用接口类型的指针,您需要保留的唯一项目是虚拟析构函数。所以实际上你的界面可以很简单:

class Testable 
{
    public:
        virtual void test() = 0;  
        virtual ~Testable();
}
于 2013-01-14T17:39:43.237 回答