1

By an interface (C# terminology) I mean an abstract class with no data members. 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++?

The C++ core guidelines [1] encourage the use of abstract class with no data members as interfaces [I.25 and C.121]. Interfaces should normally be composed entirely of public pure virtual functions and a default/empty virtual destructor [from C.121]. Hence I guess it should be declared with the struct keyword, since it only contains public members anyway.

To enable use and deletion of sub-class objects via pointers to the abstract class, the abstract class needs a public default virtual destructor [C.127]. "A polymorphic class should suppress copying" [C.67] by deleting the copy operations (copy assignment operator, copy constructor) to prevent slicing. I assume that this also extends to the move constructor and the move assignment operator, since those can also be used for slicing. For actual cloning, the abstract class may define a virtual clone method. (It's not completely clear how this should be done. Via smart pointers or owner<T*> from the Guidelines Support Library. The method using owner<T> makes no sense to me, since the examples should not compile: the derived function still does not override anything!?).

In C.129, the example uses interfaces with virtual inheritance only. If I understand correctly, it makes no difference if interfaces are derived (perhaps better: "implemented"?) using class Impl : public Interface {...}; or class Impl : public virtual Interface {...};, since they have no data that could be duplicated. The diamond problem (and related problems) don't exist for interfaces (which, I think, is the reason why languages such as C# don't allow/need multiple inheritance for classes). Is the virtual inheritance here done just for clarity? Is it good practice?

In summary, it seems that: An interface should consist only of public methods. It should declare a public defaulted virtual destructor. It should explicitly delete copy assignment, copy construction, move assignment and move construction. It may define a polymorphic clone method. I should be derived using public virtual.

One more thing that confuses me: An apparent contradiction: "An abstract class typically doesn't need a constructor" [C.126]. However, if one implements the rule of five by deleting all copy operations (following [C.67]), the class no longer has a default constructor. Hence sub-classes can never be instantiated (since sub-class constructors call base-class constructors) and thus the abstract base-class always needs to declare a default constructor?! Am I misunderstanding something?

Below is an example. Do you agree with this way to define and use an abstract class without members (interface)?

// 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
Adomas Baliuka
  • 1,384
  • 2
  • 14
  • 29
  • what is best practice is opinions. While I do agree with most I read in the coreguidelines, I do not agree with all of it. Nevertheless I can definitely recommend it. – 463035818_is_not_an_ai Dec 23 '20 at 15:53
  • you could try to get a review here: https://codereview.stackexchange.com/. Though I heard that they are rather strict, so make sure to read their rules so your question is on-topic there – 463035818_is_not_an_ai Dec 23 '20 at 15:55
  • Thanks for the comments! If you ask me, the (very deplorable) fact that C++ allows ~10 correct and ~100 correct-looking but deep down broken ways to solve any problem, does not make basic questions like "how do you define an abstract class?" opinion-based. – Adomas Baliuka Dec 23 '20 at 16:46
  • you could ask: "Is this fundamentally broken?" which isnt about opinions, but you did ask for opinions. Sometimes it is just a matter of wording... – 463035818_is_not_an_ai Dec 23 '20 at 17:21
  • `Do you agree` is opinion based. Maybe you should post on codereview.stackexchange.com instead? If you found some contradictions in some guideline, write the author and help him clarifying it. There are no easy answers where it comes to design and architecture engeneering, answers mostly come from experience. Choose the design that suits the best _for the particular problem you are solving_. `Via smart pointers or owner` Implement all possible ways and see which suits you better. Guidelines are not strict, they only show you a possible way. This question is so broad - so many questions. – KamilCuk Jan 04 '21 at 11:19
  • This is a very good question, thank you for asking this. Unfortunately, there is still no comment on `virtual` intheritance in cases like this. – Rudolf Lovrenčić Jan 27 '23 at 13:26

2 Answers2

3

You ask a lot of questions, but I'll give it a shot.

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

Nothing specifically like a C# interface exists. A C++ abstract base class comes the closest, but there are differences (for example, you will need to define a body for the virtual destructor).

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++?

As a virtual base class.

Example:

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.

There are multiple conventions for when to use a structure versus a class. The guideline I recommend (hey, you asked for opinions :D) is to use structures when you have no invariants on their data. For a base class, please use the class keyword.

"A polymorphic class should suppress copying"

Mostly true. I have written code where the client code didn't perform copies of the inherited classes, and the code worked just fine (without prohibiting them). The base classes didn't forbid it explicitly, but that was code I was writing in my own hobby project. When working in a team, it is good practice to specifically restrict copying.

As a rule, don't bother with cloning, until you find an actual use case for it in your code. Then, implement cloning with the following signature (example for my class above):

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

If this doesn't work for some reason, use another signature (return a shared_ptr for example). owner<T> is a useful abstraction, but that should be used only in corner cases (when you have a code base that imposes on you the use of raw pointers).

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

Don't try to represent the perfect C# interface in C++. C++ is more flexible than that, and rarely will you need to add a 1-to-1 implementation of a C# concept in C++.

For example, in base classes in C++ I sometimes add public non-virtual function implementations, with virtual implementations:

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?

Define the rule of 5 as needed. If code doesn't compile because you are missing a default constructor, then add a default constructor (use the guidelines only when they make sense).

Edit: (addressing comment)

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

Not necessarily. It is better (but actually "better" depends on what you agree with your team) to understand the defaults the compiler adds for you and only add construction code when it differs from that. For example, in modern C++ you can initialize members inline, often removing the need for a default constructor completely.

utnapistim
  • 26,809
  • 3
  • 46
  • 82
  • Thanks a lot for your answer! You answered most of my questions. For completion (and posterity) sake, perhaps you can edit your answer to address the "virtual inheritance" question? Also, concerning the default constructor question, **as soon as you declare a virtual destructor, you have to declare some constructor for the class to be usable in any way**, right? I'm not fuly satisfied with "do whatever you need to do to make it compile". Code that compiles may still be broken (e.g., not declaring a constructor in my case only shows up as a problem once a subclass is actually instantiated). – Adomas Baliuka Jan 04 '21 at 18:40
  • Thanks for the edit. Can you still commment on the virtual inheritance question?, i.e., "An abstract class should always be derived using `public virtual`." – Adomas Baliuka Jan 05 '21 at 08:00
  • "For example, in base classes in C++ I sometimes add public non-virtual function implementations, with [protected] virtual implementations." 1) This seems to imply that this can't be done in C#, but it can easily be done with abstract classes. 2) This is just a design pattern; it's called the Template Method pattern, and it really has nothing to do with the question about interfaces. Perhaps this detail should be removed from the answer. – Alexander Guyer Jan 05 '21 at 17:27
  • @Nerdizzle, my point was that in C++ it makes sense to add abstract base classes that do not model c# interfaces (and the template method pattern is a good example of non-virtual code added to a base class). – utnapistim Jan 06 '21 at 16:15
1

While the majority of the question has been answered, I thought I'd share some thoughts on the default constructor and the virtual inheritance.

The the class must always have a public (Or at least protected) constructor to assure that sub-classes can still call the super-constructor. Even though there is nothing to construct in the base class, this is a necessity of the syntax of C++ and conceptually makes no real difference.

I like Java as an example for interfaces and super-classes. People often wonder why Java separated abstract classes and interfaces into different syntactical types. As you probably already know though, this is due to the diamond inheritance problem, where two super-class both have the same base class and therefore copy data from the base class. Java makes this impossible be forcing data-carrying classes to be classes, not interfaces and forcing sub-classes to only inherit from one class (not interface which doesn't carry data).

We have following situation:

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

When modifyData and alsoModifyData are called on an instance of D, they will not modify the same variable as one might expect due to the compiler which will create two copies of someData for classes B and C.

To counter this problem, the concept of virtual inheritance was introduced. This means that the compiler will not just brute-force recursively build up a derived class from the super-classes members but instead see if the virtual super-classes derive from a common ancestor. Very similarly, Java has the concept of an interface, which is not allowed to own data, just functions.

But interfaces can strictly inherit from other interfaces, excluding the diamond problem to begin with. This is where Java of course differs from C++. These C++ "Interfaces" are still allowed to inherit from data-owning classes, whereas this is impossible in java.

The idea of having a "virtual inheritance", which signals that the class should be sub-classed and that data from ancestors is to be merged in case of diamond inheritance makes the necessity (or at least the idiom) of using virtual inheritance on "Interfaces" clear.

I hope this answer was (although more conceptual) helpful to you!

J. Lengel
  • 570
  • 3
  • 16