在c++ 11中声明接口的最佳方式
Best way to declare an interface in C++11
我们都知道,一些语言有接口的概念。这是Java:
public interface Testable {
void test();
}
如何在c++(或c++ 11)中以最紧凑的方式实现这一点,并减少代码噪音?我很欣赏不需要单独定义的解决方案(让标题足够)。这是一个非常简单的方法,即使我发现有bug;-)
class Testable {
public:
virtual void test() = 0;
protected:
Testable();
Testable(const Testable& that);
Testable& operator= (const Testable& that);
virtual ~Testable();
}
这只是开始…而且已经比我想要的长了如何改进它?也许在std命名空间的某个地方有一个基类就是为此而做的?
对于动态(运行时)多态性,我建议使用非虚拟接口 (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
指针从其静态基类型向下转换为其动态派生类型,并调用相应的实现(通常通过带有指向虚函数指针的表来实现)。
static_cast
完成。这种静态强制转换可以在helper类中定义,每个静态接口都派生自
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类型的接口是显式的(基于成员函数),而模板接口是隐式的(基于有效表达式)
关于:
class Testable
{
public:
virtual ~Testable() { }
virtual void test() = 0;
}
在c++中,这对子类的可复制性没有任何影响。所有这一切说的是,孩子必须实现test
(这正是你想要的接口)。你不能实例化这个类,所以你不必担心任何隐式构造函数,因为它们不能作为父接口类型直接调用。
如果你想强制子类实现析构函数,你也可以使它纯化(但你仍然必须在接口中实现它)。
还需要注意的是,如果你不需要多态破坏,你可以选择将你的析构保护为非虚的。
根据Scott Meyers (Effective Modern c++):当声明接口(或多态基类)时,您需要虚析构函数,以便通过基类指针或引用访问派生类对象的delete
或typeid
操作的正确结果。
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++中的零规则
通过将class
替换为struct
,默认情况下所有方法都将是公共的,您可以节省一行。
没有必要使构造函数受保护,因为无论如何都不能用纯虚方法实例化类。复制构造函数也是如此。编译器生成的默认构造函数将为空,因为您没有任何数据成员,并且对于派生类来说完全足够了。
您对=
操作符的关注是正确的,因为编译器生成的操作符肯定会做错误的事情。实际上,没有人担心它,因为将一个接口对象复制到另一个接口对象是没有意义的;这不是经常发生的错误。
最终结果只比Java的等效代码长一行:
struct Testable {
virtual void test() = 0;
virtual ~Testable();
};
请记住,如果您不管理指针、句柄和/或类的所有数据成员都有自己的析构函数来管理任何清理,则"三规则"是不必要的。同样在虚基类的情况下,因为基类永远不能被直接实例化,所以如果你只想定义一个没有数据成员的接口,就没有必要声明构造函数。编译器默认值很好。如果您计划在接口类型的指针上调用delete
,那么您需要保留的唯一项就是虚析构函数。所以在现实中,你的界面可以像这样简单:
class Testable
{
public:
virtual void test() = 0;
virtual ~Testable();
}
- 在c代码之间共享数据的最佳方式
- 使用QQuickFramebufferObject时同步数据的最佳方式是什么
- 从嵌套在std::映射中的std::列表中删除元素的最佳方式
- 如果条件为TRUE(最佳方式?),则在do while循环中后置增量
- 在reactor中存储eventHandlers的最佳方式是什么
- 在AVX通道中混洗的最佳方式
- 从 T 创建 std::future 的最佳方式<T>
- C++:使用 std::unique_ptr 访问重载运算符++的最佳方式?
- 对列表列表中的元素进行分组的最佳方式
- 利用 GPU 的最佳方式
- 使用 QT C++过滤大数据的最佳方式
- 算法设计:用边界数字表示 2D 网格的最佳方式,以C++?
- 在C++中共享键值对的最佳方式
- 为Catch2中的外部文本文件指定路径的最佳方式
- 代表Quarto棋盘游戏棋子的最佳方式
- 等待线程的最佳方式是什么
- 将uint8_t*buffer和size_tbufferlen从C++传递到C中的API函数的最佳方式是什么
- 创建控制台菜单C++的最佳方式
- 只显示片段着色器的最佳方式是什么
- 复制文件的最佳方式是什么,以便我可以在复制过程中轻松取消复制?