【问题标题】:Best way to declare an interface in C++11在 C++11 中声明接口的最佳方法
【发布时间】:2012-12-28 17:12:22
【问题描述】:

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

【问题讨论】:

  • 接口通常不可复制,也不能直接构造,你真的在​​声明接口吗?
  • 这里我只是想把它们隐藏起来,让孩子们自己决定。
  • 为什么需要受保护的定义?
  • @MerickOWA 所以我想现在很清楚我为什么要保护它们——让孩子们可以复制。 =delete 怎么样 - 它是继承的吗?方法删除后可以实现吗?

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


【解决方案1】:

对于动态(运行时)多态性,我建议使用 Non-Virtual-Interface (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 类型接口是显式的(基于成员函数),而模板接口是隐式的(基于有效表达式)

【讨论】:

  • 我读过 NVI 在你有一些通用代码时很好,比如前置或后置条件。 NVI 在接口声明中有何改进?
  • 这是 Herb Sutter 首选的方法。不确定我是否同意它,因为它似乎使事情变得不必要地复杂化,但他提出了一些好的观点:gotw.ca/publications/mill18.htm
  • 它将允许您稍后将前置或后置条件添加到您的类中,而派生类不必调整它们的代码。这种灵活性是 NVI 的一大优势
  • "用 const 版本写非 const 版本" 嗯,这是一个用来重用复杂代码的工具,但在这种情况下,你只是让它变得更复杂了。
  • @vargonian 是的,多态性仍然通过公共虚拟接口维护。但是,使用带有受保护虚拟实现的公共非虚拟接口允许在基类中实现各种断言。参见例如本专栏作者 Herb Sutter:gotw.ca/publications/mill18.htm
【解决方案2】:

怎么样:

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

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

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

另外请注意,如果您不需要多态破坏,您可以选择将您的析构函数设置为非虚拟的。

【讨论】:

  • @elmes:不。它需要一个定义(但您可以像 Mark B 所示的那样将其留空)
  • @elmes:析构函数可以是纯虚函数,但必须提供定义(两者不互斥)。接口是可复制的这一事实并不意味着对象是可复制的,因此该接口并不意味着具有这种能力。事实上,在接口级别复制会导致slicing,在任何时候都不是一个好主意。
  • @MarkB 这是一个断章取义的可怕陈述,我什至不确定在什么情况下这样的陈述甚至有用。 =delete 适用于不是(普通、非复制和非移动)构造函数/析构函数的任何成员函数。
  • @Steve-o:不,不应该是=delete=delete 表示调用它是不合法的(尝试调用它会出错。=0 表示它是合法的,但必须由子类定义。
  • @Cornstalks - virtual ~Testable() = default; 比在 C++ 11 中定义自己的身体更可取
【解决方案3】:

根据 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;

};

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

【讨论】:

    【解决方案4】:

    class替换成struct,默认所有方法都是公开的,可以省一行。

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

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

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

    最终结果只比 Java 等价的多一行:

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

    【讨论】:

    • 析构函数...应该始终是公共的和虚拟的,或者受保护的和非虚拟的。为什么要互斥?
    • @elmes,如果析构函数是公共的,你会很想使用它,它需要是虚拟的才能正常工作。如果它受到保护,则无需将其设为虚拟,因为只有派生类可以调用它,并且它们会自动调用基类析构函数。当然,将受保护的析构函数设为虚拟并没有什么坏处,只是没有任何好处。我没有制定规则,我只是重复它。
    • 使用 dtor 是什么意思?只需通过基指针 (delete b) 删除派生类,还是在 placement new (b-&gt;~b()) 之后显式调用 dtor?有人想以这种方式使用它吗? :)
    • @elmes,是的,我的意思是通过基指针删除派生类。您是否需要它取决于您如何处理对象的生命周期。假设你会需要它总是更安全,即使你从来不需要它。
    【解决方案5】:

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

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

    【讨论】:

    • 如果有可能通过其接口指针删除对象,则仍然需要虚拟析构函数。
    • 是的,如果析构函数是公共的,则可以说它应该是虚拟的,或者如果它不是虚拟的,则应该是受保护的。
    • 为什么要让 dtor 受保护而不是公开?
    • 虚拟受保护的析构函数,正如您现在在回答中所说的那样,没有用。它只能从派生类中调用,因为它是受保护的,所以它不需要是虚拟的。
    • 谢谢。这就是我开始这个话题的原因 - 将所有这些细节总结在一个地方
    猜你喜欢
    • 1970-01-01
    • 2013-08-12
    • 1970-01-01
    • 1970-01-01
    • 2014-02-02
    • 1970-01-01
    • 2016-05-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多