在 C++ 中定义接口(抽象 类 没有成员)

Defining interfaces (abstract classes without members) in C++

接口(C# 术语)是指没有数据成员的抽象 class。因此,这样的 class 仅指定子 classes 必须实现的契约(一组方法)。我的问题是:How to implement such a class correctly in modern C++?

C++ 核心指南 [1] 鼓励使用没有数据成员的抽象 class 作为接口 [I.25 和 C.121]。接口通常应该完全由 public 纯虚函数和一个 default/empty 虚析构函数 [来自 C.121]。因此我想它应该用 struct 关键字声明,因为它只包含 public 成员。

要通过指向抽象 class 的指针启用子 class 对象的使用和删除,抽象 class 需要一个 public 默认虚拟析构函数 [C. 127]。 “多态 class 应该抑制复制” [C.67] 通过删除复制操作(复制赋值运算符,复制构造函数)来防止切片。我假设这也扩展到移动构造函数和移动赋值运算符,因为它们也可以用于切片。对于实际的克隆,抽象 class 可以定义一个虚拟 clone 方法。 (尚不完全清楚应该如何完成。通过智能指针或指南支持库中的 owner<T*>使用 owner<T> 的方法对我来说毫无意义,因为示例不应该编译:派生函数仍然没有override任何东西!?)。

在 C.129 中,该示例仅使用具有虚拟继承的接口。如果我理解正确,如果使用 class Impl : public Interface {...};class Impl : public virtual Interface {...}; 派生接口(也许更好:“实现”?)没有区别,因为它们没有可以复制的数据。接口不存在菱形问题(及相关问题)(我认为,这就是 C# 等语言不 allow/need classes 多重继承的原因)。这里的虚拟继承只是为了清楚起见吗?这是好的做法吗?

综上所述,似乎: 一个接口应该只包含 public 个方法。它应该声明一个 public 默认的虚拟析构函数。它应该明确删除复制赋值、复制构造、移动赋值和移动构造。它可以定义一个多态克隆方法。我应该使用 public virtual.

导出

还有一件事让我很困惑: 一个明显的矛盾:“抽象 class 通常不需要构造函数”[C.126]。但是,如果通过删除所有复制操作(遵循 [C.67])来实现五规则,则 class 不再具有默认构造函数。因此 sub-classes 永远不能被实例化(因为 sub-class 构造函数调用 base-class 构造函数)因此 抽象 base-class 总是需要声明默认构造函数?!我是不是误会了什么?

下面是一个例子。 您同意这种定义和使用没有成员(接口)的抽象class的方式吗?

// 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

你问了很多问题,但我会试一试。

By an interface (C# terminology) I mean an abstract class with no data members.

不存在特别类似于 C# 接口的东西。 C++ 抽象基础 class 最接近,但也有差异(例如,您需要为虚拟析构函数定义一个主体)。

Thus, such a class only specifies a contract (a set of methods) that sub-classes must implement. My question is: How to implement such a class correctly in modern C++?

作为虚拟基地class。

示例:

class OutputSink
{
public:
    
    ~OutputSink() = 0;

    // contract:
    virtual void put(std::vector<std::byte> const& bytes) = 0;
};

OutputSink::~OutputSink() = default;

Hence I guess it should be declared with the struct keyword, since it only contains public members anyway.

关于何时使用结构与 class 有多种约定。我推荐的指导方针(嘿,你问过意见 :D)是在你的数据没有不变量时使用结构。对于基础 class,请使用 class 关键字。

"A polymorphic class should suppress copying"

大部分是正确的。我编写了客户端代码不执行继承的 classes 副本的代码,并且代码工作正常(不禁止它们)。基础 classes 并没有明确禁止它,但那是我在自己的爱好项目中编写的代码。在团队中工作时,最好特别限制复制。

通常,在您在代码中找到克隆的实际用例之前,不要为克隆操心。然后,使用以下签名实施克隆(上面我的 class 的示例):

virtual std::unique_ptr<OutputSink> OutputSink::clone() = 0;

如果由于某种原因这不起作用,请使用另一个签名(例如 return a shared_ptr)。 owner<T> 是一个有用的抽象,但只能在极端情况下使用(当您的代码库强制您使用原始指针时)。

An interface should consist only of public methods. It should declare [...]. It should [...]. It should be derived using public virtual.

不要试图用 C++ 表示完美的 C# 接口。 C++ 比这更灵活,您很少需要在 C++ 中添加 C# 概念的一对一实现。

例如,在 C++ 的基础 classes 中,我有时会添加 public 非虚函数实现,使用虚函数实现:

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
};

thus the abstract base-class always needs to declare a default constructor?! Am I misunderstanding something?

根据需要定义5的规则。如果代码由于缺少默认构造函数而无法编译,请添加默认构造函数(仅在有意义时使用指南)。

编辑:(处理评论)

as soon as you declare a virtual destructor, you have to declare some constructor for the class to be usable in any way

不一定。最好(但实际上“更好”取决于您与您的团队达成一致)了解编译器为您添加的默认值,并且仅在与此不同时才添加构造代码。例如,在现代 C++ 中,您可以内联初始化成员,通常完全不需要默认构造函数。

虽然大部分问题已经得到解答,但我想我会分享一些关于默认构造函数和虚拟继承的想法。

class 必须始终有一个 public (或至少受保护)构造函数以确保子 class 仍然可以调用超级构造函数。尽管在基础 class 中没有什么可构造的,但这是 C++ 语法的必要条件,并且在概念上没有真正的区别。

我喜欢 Java 作为接口和超级 class 的例子。人们常常想知道为什么 Java 将抽象 class 和接口分成不同的语法类型。正如您可能已经知道的那样,这是由于菱形继承问题造成的,其中两个超级 class 都具有相同的基 class,因此从基 class 复制数据。 Java 使得这不可能强制携带数据的 classes 成为 classes,而不是接口并强制子 classes 仅继承一个 class (不是不​​携带数据的接口)。

我们有以下情况:

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 时,由于编译器将为 classes B 和 C 创建 someData 的两个副本,因此它们不会像人们预期的那样修改同一个变量。

为了解决这个问题,引入了虚拟继承的概念。这意味着编译器不仅会暴力递归地从 super-classes 成员中构建一个派生的 class,而是查看虚拟 super-classes 是否从一个公共的派生祖先。非常相似,Java有接口的概念,不允许拥有数据,只有功能。

但是接口可以严格继承自其他接口,首先排除菱形问题。这就是 Java 当然不同于 C++ 的地方。这些 C++“接口”仍然允许从拥有数据的 classes 继承,而这在 java.

中是不可能的

拥有“虚拟继承”的想法,这表明 class 应该被子classed 并且在钻石继承的情况下来自祖先的数据将被合并使得在“接口”上使用虚拟继承的必要性(或至少是成语)很清楚。

我希望这个答案(虽然更概念化)对您有所帮助!