关于c ++:在C ++ 11中声明接口的最佳方式

Best way to declare an interface in C++11

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

1
2
3
public interface Testable {
  void test();
}

如何在C++(或C++ 11)中以最紧凑的方式和很少的代码噪声来实现这一点?我希望有一个不需要单独定义的解决方案(让标题足够)。这是一个非常简单的方法,即使我觉得有问题;-)

1
2
3
4
5
6
7
8
9
class Testable {
public:
  virtual void test() = 0;
protected:
  Testable();
  Testable(const Testable& that);
  Testable& operator= (const Testable& that);
  virtual ~Testable();
}

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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指针从静态基类型向下转换为动态派生类型,并调用相应的实现(通常通过带有指向虚拟函数的指针的表发生)。

对于静态(编译时多态性),我建议使用奇怪的循环模板模式(CRTP)。这是相当复杂的,因为从基础到衍生的动态多孔隙模型的自动向下铸造必须与static_cast一起完成。可以在每个静态接口派生的帮助器类中定义此静态强制转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
};

然后定义一个静态接口,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
};

最后,您将生成一个从接口派生的实现,该接口本身作为参数

1
2
3
4
5
6
7
8
9
class StaticImplementation
:
    public StaticInterface< StaticImplementation >
{
private:
    // implementation
    friend class StaticInterface< StaticImplementation > ;
    void do_fun() { /* your implementation here */ }
};

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

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

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


如何:

1
2
3
4
5
6
class Testable
{
public:
    virtual ~Testable() { }
    virtual void test() = 0;
}

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

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

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


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

1
virtual ~Testable() = default;

但是,用户声明的析构函数禁止移动操作,因此要支持移动操作,需要添加:

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

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

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

最终结果是:

1
2
3
4
5
6
7
8
9
10
11
12
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++中的零规则


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

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

因为编译器生成的=操作符肯定会出错,所以您应该关注它。实际上,没有人会担心它,因为将一个接口对象复制到另一个接口对象是没有意义的;这不是一个常见的错误。

可继承类的析构函数应始终为public和virtual,或protected和non-virtual。在这种情况下,我更喜欢公开的和虚拟的。

最后的结果只比Java等价的一行长:

1
2
3
4
struct Testable {
    virtual void test() = 0;
    virtual ~Testable();
};


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

1
2
3
4
5
6
class Testable
{
    public:
        virtual void test() = 0;  
        virtual ~Testable();
}