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

eme*_*esx 46 c++ polymorphism interface abstract c++11

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

public interface Testable {
  void test();
}
Run Code Online (Sandbox Code Playgroud)

如何以最紧凑的方式在C++(或C++ 11)中实现这一点并且代码噪声很小?我很欣赏一个不需要单独定义的解决方案(让标题足够).这是一个非常简单的方法,即使我发现越野车;-)

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

这只是一个开始......而且已经超过了我想要的时间.怎么改进呢?也许在std命名空间的某个地方有一个基类专为此而设计?

Tem*_*Rex 42

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

关于动态多态的好处是你可以-at运行时传递任何派生类,其中指向接口基类的指针或引用.运行时系统将自动将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
};
Run Code Online (Sandbox Code Playgroud)

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

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
};
Run Code Online (Sandbox Code Playgroud)

最后,您创建一个实现,该实现派生自作为参数的接口

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

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

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

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

  • 当你有一些常见的代码,比如前置或后置条件时,我读过NVI很好.NVI在接口声明中有哪些改进? (2认同)

Mar*_*k B 40

关于什么:

class Testable
{
public:
    virtual ~Testable() { }
    virtual void test() = 0;
}
Run Code Online (Sandbox Code Playgroud)

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

如果你希望强制那些子类实现一个析构函数,你也可以使它变得纯粹(但是你仍然必须在接口中实现它).

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

  • @elmes:析构函数可以是*pure virtual*,但它必须提供一个定义(两者不是独占的).接口是可复制的这一事实并不意味着对象是,因此该接口并不意味着该功能.事实上,在界面级别进行复制会导致*切片*并且在任何时候都不是一个好主意. (13认同)
  • @Cornstalks - `virtual~Testable()= default;`比在C++ 11中定义自己的主体更可取 (8认同)
  • @ Steve-o:不,它不应该是`= delete`.`= delete`表示调用它是不合法的(尝试调用它时会出错.` = 0`表示它是合法的,但必须由子类定义. (4认同)
  • @elmes:不.它需要一个定义(但你可以把它留空,就像Mark B所示) (2认同)

Aah*_*zbg 23

根据斯科特迈尔斯(有效的现代C++):当声明接口(或多态性基类),则需要虚析构函数,对于像操作的正确的结果deletetypeid通过基类的指针或引用访问的派生类对象上.

virtual ~Testable() = default;
Run Code Online (Sandbox Code Playgroud)

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

Testable(Testable&&) = default; 
Testable& operator=(Testable&&) = default;
Run Code Online (Sandbox Code Playgroud)

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

Testable(const Testable&) = default;
Testable& operator=(const Testable&) = default;
Run Code Online (Sandbox Code Playgroud)

最终的结果是:

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;

};
Run Code Online (Sandbox Code Playgroud)


Mar*_*som 11

通过更换字classstruct,所有的方法将默认为公开,你可以节省线.

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

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

可继承类的析构函数应始终为public和virtual,或者是protected和非虚拟的.在这种情况下,我更喜欢公共和虚拟.

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

struct Testable {
    virtual void test() = 0;
    virtual ~Testable();
};
Run Code Online (Sandbox Code Playgroud)


Jas*_*son 7

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

class Testable 
{
    public:
        virtual void test() = 0;  
        virtual ~Testable();
}
Run Code Online (Sandbox Code Playgroud)

  • 如果有可能通过其接口指针删除对象,则仍然需要虚拟析构函数. (10认同)
  • 是的,可以说析构函数应该是虚拟的,如果它是公开的,或者如果它不是虚拟的则是受保护的. (3认同)