在C ++ 11中声明接口的最佳方法

时间:2013-01-14 17:35:42

标签: c++ interface c++11 polymorphism abstract

众所周知,某些语言具有接口的概念。这是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命名空间中的某个基类是为此而制作的?

5 个答案:

答案 0 :(得分:40)

对于动态(运行时)多态,我建议使用非虚拟接口(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 */ }
};

关于动态多态的好处是你可以-at runtime-传递任何派生类,其中指向接口基类的指针或引用。运行时系统将自动将this指针从其静态基类型转发为其动态派生类型,并调用相应的实现(通常通过指向虚函数的表来实现)。

对于静态(编译时多态),我建议使用奇怪的重复模板模式(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类型接口是显式的(基于成员函数),模板接口是隐式的(基于有效表达式)

答案 1 :(得分:37)

怎么样:

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

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

如果您希望强制执行子类实现析构函数,您也可以将其设置为纯粹的(但您仍需要在界面中实现它)。

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

答案 2 :(得分:19)

根据Scott Meyers(Effective Modern 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;

};

这里有另一篇有趣的文章:The Rule of Zero in C++

答案 3 :(得分:11)

class替换为struct,默认情况下所有方法都是公开的,您可以保存一行。

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

你关注=运算符是正确的,因为编译器生成的运算符肯定会做错误的事情。实际上,没有人会担心它,因为将一个界面对象复制到另一个界面对象永远不会有意义这不是常见的错误。

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

最终结果只比Java等价物长一行:

struct Testable {
    virtual void test() = 0;
    virtual ~Testable();
};

答案 4 :(得分:7)

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

class Testable 
{
    public:
        virtual void test() = 0;  
        virtual ~Testable();
}
相关问题