【发布时间】:2021-04-02 05:32:17
【问题描述】:
接口(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 {...}; 或class Impl : public virtual Interface {...}; 派生接口(也许更好:“实现”?)没有区别,因为它们没有可以复制的数据。接口不存在菱形问题(和相关问题)(我认为,这是诸如 C# 之类的语言不允许/需要类的多重继承的原因)。这里的虚拟继承是为了清楚起见吗?这是个好习惯吗?
总而言之,似乎:
接口应该只包含公共方法。它应该声明一个公共的默认虚拟析构函数。它应该明确删除复制分配、复制构造、移动分配和移动构造。它可以定义一个多态克隆方法。我应该使用public virtual 导出。
还有一件事让我感到困惑: 一个明显的矛盾:“抽象类通常不需要构造函数”[C.126]。但是,如果通过删除所有复制操作(遵循 [C.67])来实现五规则,则该类不再具有默认构造函数。因此子类永远不能被实例化(因为子类构造函数调用基类构造函数),因此抽象基类总是需要声明一个默认构造函数?!我误解了什么吗? p>
下面是一个例子。 您是否同意这种方式来定义和使用没有成员(接口)的抽象类?
// 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
【问题讨论】:
-
最佳实践是意见。虽然我同意我在核心指南中读到的大部分内容,但我并不完全同意。不过我绝对可以推荐它。
-
您可以尝试在这里获得评论:codereview.stackexchange.com。虽然我听说他们相当严格,所以一定要阅读他们的规则,这样你的问题就会成为主题
-
感谢 cmets!如果你问我,C++ 允许 ~10 种正确和 ~100 种看起来正确但内心深处破碎的方法来解决任何问题(非常可悲)这一事实,并没有提出诸如“你如何定义抽象类?”之类的基本问题。基于意见。
-
您可能会问:“这是否从根本上破坏了?”这不是关于意见,但你确实征求意见。有时这只是措辞的问题......
-
Do you agree是基于意见的。也许您应该改为在 codereview.stackexchange.com 上发帖?如果您在某些指南中发现一些矛盾,请写信给作者并帮助他澄清。在设计和建筑工程方面没有简单的答案,答案大多来自经验。选择最适合您正在解决的特定问题的设计。Via smart pointers or owner<T*>实施所有可能的方式,看看哪种方式更适合您。指南并不严格,它们只向您展示一种可能的方式。这个问题太宽泛了——问题太多了。
标签: c++ abstract-class cpp-core-guidelines rule-of-five