接口(C#术语)指的是没有数据成员的抽象类。因此,这样的类只指定子类必须实现的约定(一组方法)。我的问题是:如何在现代C++中正确实现这样一个类
C++核心准则[1]鼓励使用没有数据成员的抽象类作为接口[I.25和C.121]。接口通常应该完全由公共纯虚拟函数和默认/空虚拟析构函数组成[来自C.121],因此我认为它应该用struct
关键字声明,因为它只包含公共成员。
为了能够通过指向抽象类的指针使用和删除子类对象,抽象类需要一个公共默认虚拟析构函数[C.127];多态类应该抑制"复制";[C.67]通过删除复制操作(复制赋值运算符、复制构造函数)来防止切片。我认为这也扩展到了move构造函数和move赋值运算符,因为它们也可以用于切片。对于实际的克隆,抽象类可以定义虚拟clone
方法。(目前还不完全清楚应该如何做到这一点。通过智能指针或指南支持库中的owner<T*>
。使用owner<T>
的方法对我来说毫无意义,因为示例不应该编译:派生函数仍然没有override
!?)。
在C.129中,该示例仅使用具有虚拟继承的接口。如果我理解正确的话,使用class Impl : public Interface {...};
或class Impl : public virtual Interface {...};
派生接口(也许更好:"实现"?)没有什么区别,因为它们没有可以复制的数据。接口不存在钻石问题(以及相关问题)(我认为,这就是为什么C#等语言不允许/需要类的多重继承的原因)。这里的虚拟继承只是为了清楚吗?这是好的做法吗?
总之,似乎:接口应该只由公共方法组成。它应该声明一个公共默认的虚拟析构函数。应明确删除复制赋值、复制构造、移动赋值和移动构造。它可以定义多态克隆方法。I应该使用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
你问了很多问题,但我会试一试。
接口(C#术语)指的是没有数据成员的抽象类。
不存在像C#接口这样的特殊接口。C++抽象基类最接近,但也有区别(例如,您需要为虚拟析构函数定义一个主体)。
因此,这样的类只指定子类必须实现的约定(一组方法)。我的问题是:如何在现代C++中正确地实现这样一个类?
作为一个虚拟基类。
示例:
class OutputSink
{
public:
~OutputSink() = 0;
// contract:
virtual void put(std::vector<std::byte> const& bytes) = 0;
};
OutputSink::~OutputSink() = default;
因此,我认为应该用struct关键字声明它,因为它只包含公共成员。
对于何时使用结构与类,有多种约定。我建议的指导原则(嘿,你征求了意见:D)是在数据上没有不变量的情况下使用结构。对于基类,请使用class
关键字。
"多态类应该抑制"复制";
大部分为真。我编写的代码中,客户端代码不执行继承类的副本,并且代码运行良好(没有禁止它们)。基类并没有明确禁止它,但这是我在自己的业余项目中编写的代码。在团队中工作时,最好特别限制复制。
通常,在您的代码中找到它的实际用例之前,不要麻烦克隆。然后,使用以下签名实现克隆(例如上面的类):
virtual std::unique_ptr<OutputSink> OutputSink::clone() = 0;
如果由于某种原因这不起作用,请使用另一个签名(例如,返回shared_ptr)。owner<T>
是一个有用的抽象,但它应该只在极端情况下使用(当你有一个强制使用原始指针的代码库时)。
接口应该只由公共方法组成。它应该声明[…]。它应该[…],它应该使用公共虚拟派生。
不要试图在C++中表示完美的C#接口。C++比这更灵活,而且很少需要在C++中添加C#概念的1对1实现。
例如,在C++中的基类中,我有时会添加公共的非虚拟函数实现,其中包括虚拟实现:
class OutputSink
{
public:
void put(const ObjWithHeaderAndData& o) // non-virtual
{
put(o.header());
put(o.data());
}
protected:
virtual void put(ObjectHeader const& h) = 0; // specialize in implementations
virtual void put(ObjectData const& d) = 0; // specialize in implementations
};
因此抽象基类总是需要声明一个默认构造函数?!我是不是误解了什么?
根据需要定义5的规则。如果由于缺少默认构造函数而导致代码无法编译,则添加一个默认构造函数(只有在有意义时才使用这些准则)。
编辑:(寻址注释)
一旦您声明了一个虚拟析构函数,您就必须声明一些构造函数,以便该类可以以任何方式使用
不一定。更好的做法是(但实际上"更好"取决于你对团队的看法)理解编译器为你添加的默认值,并且只在不同的情况下添加构造代码。例如,在现代C++中,您可以内联初始化成员,通常完全不需要默认构造函数。
虽然大部分问题都已经回答了,但我想分享一下关于默认构造函数和虚拟继承的一些想法。
类必须始终有一个公共(或者至少是受保护的)构造函数,以确保子类仍然可以调用超级构造函数。尽管基类中没有任何可构造的内容,但这是C++语法的必要性,在概念上没有什么实际区别。
我喜欢Java作为接口和超类的例子。人们经常想知道为什么Java将抽象类和接口分离成不同的语法类型。不过,您可能已经知道,这是由于钻石继承问题,即两个超类都有相同的基类,因此从基类复制数据。Java强制数据承载类为类,而不是接口,并强制子类只能从一个类继承(而不是不承载数据的接口),从而使这一点变得不可能。
我们有以下情况:
struct A {
int someData;
A(): someData(0) {}
};
struct B : public A {
virtual void modifyData() = 0;
};
struct C : public A {
virtual void alsoModifyData() = 0;
};
struct D : public B, public C {
virtual void modifyData() { someData += 10; }
virtual void alsoModifyData() { someData -= 10; }
};
当在D的实例上调用modifyData和alsoModifyData时,由于编译器将为类B和C创建someData的两个副本,它们不会像预期的那样修改同一变量。
为了解决这个问题,引入了虚拟继承的概念。这意味着编译器不仅会使用蛮力递归地从超类成员构建派生类,还会查看虚拟超类是否派生自共同的祖先。同样,Java也有接口的概念,它不允许拥有数据,只允许拥有函数。
但是接口可以严格继承其他接口,从一开始就排除了钻石问题。这就是Java与C++的不同之处。这些C++";接口";仍然允许从拥有数据的类继承,而这在java中是不可能的。
拥有";"虚拟继承";,这表示该类应该被细分并且在钻石继承的情况下来自祖先的数据将被合并,这使得在"上使用虚拟继承的必要性(或者至少是习惯用法);接口";清楚的
我希望这个答案(虽然更具概念性)对你有帮助!