接口(C# 术语)是指没有数据成员的抽象类。因此,这样的类只指定了子类必须实现的契约(一组方法)。我的问题是:如何在现代 C++ 中正确实现这样的类?
C++ 核心指南 [1] 鼓励使用没有数据成员的抽象类作为接口 [I.25 和 C.121]。接口通常应该完全由公共纯虚函数和默认/空虚析构函数[来自 C.121] 组成。因此我想它应该用struct
关键字声明,因为它只包含公共成员。
为了通过指向抽象类的指针来使用和删除子类对象,抽象类需要一个公共的默认虚拟析构函数[C.127]。“多态类应该禁止复制”[C.67] 通过删除复制操作(复制赋值运算符、复制构造函数)来防止切片。我假设这也扩展到移动构造函数和移动赋值运算符,因为它们也可以用于切片。对于实际的克隆,抽象类可以定义一个虚clone
方法。(目前尚不完全清楚应该如何完成。通过智能指针或owner<T*>
指南支持库。使用的方法owner<T>
对我来说毫无意义,因为示例不应该编译:派生函数仍然没有override
任何作用!?)。
在 C.129 中,该示例仅使用具有虚拟继承的接口。如果我理解正确,那么使用class Impl : public Interface {...};
or派生接口(也许更好:“实现”?)没有区别class Impl : public virtual Interface {...};
,因为它们没有可以复制的数据。接口不存在菱形问题(和相关问题)(我认为,这是诸如 C# 之类的语言不允许/需要类的多重继承的原因)。这里的虚拟继承是为了清楚起见吗?这是好习惯吗?
总之,似乎:
一个接口应该只包含公共方法。它应该声明一个公共的默认虚拟析构函数。它应该明确删除复制分配、复制构造、移动分配和移动构造。它可以定义一个多态克隆方法。我应该使用public virtual
.
还有一件事让我感到困惑:一个明显的矛盾:“抽象类通常不需要构造函数”[C.126]。但是,如果通过删除所有复制操作(遵循 [C.67])来实现五规则,则该类不再具有默认构造函数。因此子类永远不能被实例化(因为子类构造函数调用基类构造函数),因此抽象基类总是需要声明一个默认构造函数?!我是不是误会了什么?
下面是一个例子。您是否同意这种方式来定义和使用没有成员(接口)的抽象类?
// C++17
/// An interface describing a source of random bits.
// The type `BitVector` could be something like std::vector<bool>.
#include <memory>
struct RandomSource { // `struct` is used for interfaces throughout core guidelines (e.g. C.122)
virtual BitVector get_random_bits(std::size_t num_bits) = 0; // interface is just one method
// rule of 5 (or 6?):
RandomSource() = default; // needed to instantiate sub-classes !?
virtual ~RandomSource() = default; // Needed to delete polymorphic objects (C.127)
// Copy operations deleted to avoid slicing. (C.67)
RandomSource(const RandomSource &) = delete;
RandomSource &operator=(const RandomSource &) = delete;
RandomSource(RandomSource &&) = delete;
RandomSource &operator=(RandomSource &&) = delete;
// To implement copying, would need to implement a virtual clone method:
// Either return a smart pointer to base class in all cases:
virtual std::unique_ptr<RandomSource> clone() = 0;
// or use `owner`, an alias for raw pointer from the Guidelines Support Library (GSL):
// virtual owner<RandomSource*> clone() = 0;
// Since GSL is not in the standard library, I wouldn't use it right now.
};
// Example use (class implementing the interface)
class PRNG : public virtual RandomSource { // virtual inheritance just for clarity?
// ...
BitVector get_random_bits(std::size_t num_bits) override;
// may the subclass ever define copy operations? I guess no.
// implemented clone method:
// owner<PRNG*> clone() override; // for the alternative owner method...
// Problem: multiple identical methods if several interfaces are inherited,
// each of which requires a `clone` method?
//Maybe the std. library should provide an interface
// (e.g. `Clonable`) to unify this requirement?
std::unique_ptr<RandomSource> clone() override;
//
// ... private data members, more methods, etc...
};
[1]: https://github.com/isocpp/CppCoreGuidelines, commit 2c95a33fefae87c2222f7ce49923e7841faca482