1

I am trying to find the best design for several classes that store data. For all I know I could do something with inheritance:

struct Data
{
    int a;
    int b;
    int c;
};

struct DerivedData : public Data
{
    int d;
};

struct AnotherDerivedData : public Data
{
    int e;
};

Or with composition:

struct ComposedData
{
    Data data;
    int d;
};

struct AnotherComposedData
{
    Data data;
    int e;
};

I am leaning towards inheritance because there is a relationship between Data and DerivedData. But I was wondering if it was good design since there are absolutely no functions to be inherited ?

Moreover, Data should not be used on its own, so I was wondering if I could make it abstract and if it was worth it ? I've read that to do that, you should make a pure virtual destructor or preferably a protected constructor:

struct Data
{
    int a;
    int b;
    int c;

protected:
    Data() = default;
};

But what I don't like here is that it introduces a bit of complexity in an otherwise simple struct. It is not even a POD (if I understood what PODs are correctly).

Any advice on what would be the best implementation ? Are there any other ways to do it ?

Edit: I realize I was not clear enough about what is mainly bothering me: There is a "is-a" relationship between DerivedData and Data. But for some reason, I am feeling like inheritance is a bit "excessive" because there are no functions to be overriden (and even no function at all). But maybe this is a misconception on my part and that inheritance is totally appropriate here ?

PrOpoLo
  • 85
  • 5
  • "Data should not be used on its own": does that mean a "consumer" of your Composed/DerivedData should also not be able to deal with a, b, or c? – Marcus Müller Nov 09 '21 at 10:12
  • 4
    there is too little context to be not opinion based. What is `Data`, `DerivedData` and `AnotherDerivedData`? What is the relevance of pod-ness? (once you have a base class it is no pod, so why bother if `Data` is pod?) – 463035818_is_not_an_ai Nov 09 '21 at 10:18
  • yep, in modern C++, being plain old data doesn't often matter – other things sometimes matter, like being const initializable, or default construbtible (or copy-constructible, movable… depends on context) – Marcus Müller Nov 09 '21 at 10:22
  • 1
    Does `Data` represent an Engine and `DerivedData` a Car, or is `Data` a Car and `DerivedData` a Porshe? It really depends on what you are modelling and what do you want from it. – KamilCuk Nov 09 '21 at 10:39
  • @MarcusMüller What I mean is that a client should generally use a Composed/DerivedData and not a mere Data. Data contains some basic information that is required but not sufficient (at least for what I want to do, but I guess maybe I should allow clients to use only Data if they want to). Clients should be able to deal with a, b or c. – PrOpoLo Nov 09 '21 at 11:31
  • @463035818_is_not_a_number Data is for example a Car and DerivedData a Porshe. There is no relevance to pod-ness here, my main concern was that maybe inheritance should only be used when overriding some fonctionnality (i.e. when there is at least one virtual function) and not simply to store more specific members. – PrOpoLo Nov 09 '21 at 11:35
  • @KamiCuk It's the latter, Data is a Car and DerivedData a Porshe. There is a "is a" relationship between DerivedData and Data, that's why I was leaning more towards inheritance. But as I said in another comment, I am not sure if inheritance is appropriate here or if it is "excessive" since there are no functions to be overriden. – PrOpoLo Nov 09 '21 at 11:38
  • it seems you are focused too much on implementation details. First you should consider what is modeled by inheritance vs composition. Does a `Porsche` contain a `Car` or is there an "is-a" relation between them? That should give you a good start and if you later realize that the design should change then you need to change it. Its not a sin to redesign – 463035818_is_not_an_ai Nov 09 '21 at 11:39

1 Answers1

2

API

AFAIU Data is supposed to be "private" for clients. However, the approach with composition makes them know about it - they type ComposedData{}.data.a instead of ComposedData{}.a for the inheritance case.

Safety and performance

Data should not be used on its own, so I was wondering if I could make it abstract and if it was worth it ? I've read that to do that, you should make a pure virtual destructor or preferably a protected constructor

Virtual destructors dispatch through a virtual table, in the described case it gives you nothing but overhead. To disallow using raw Data, instead just hide its destructor from non-derivers:

class Data {
protected: ~Data() = default;
public: int a, b, c;
};

struct ComposedData: public Data { int d; }; // has access to the protected ~Data()

int main() {
  Data raw; // doesn't compile: raw.~Data() is inaccessible
  ComposedData composed; // OK (~ComposedData() is public)
}

Moreover, protected destructors, unlike protected constructors, prevent mistakes like this one:

struct NonTrivialData: public Data { std::string non_trivial_dtor; };

std::unique_ptr<Data> type_erased{new NonTrivialData{1, 2, 3, "i won't be deallocated"}};
// ...because ~Data() knows nothing about NonTrivialData::non_trivial_dtor

(I didn't use the usual make_unique because it cannot perform aggregate initialization.)

A protected destructor disallows calling ~Data() from the outside => the code doesn't compile.

A virtual destructor dispatches ~Data() as ~NonTrivialData() at runtime => the destruction is done properly. However, as far as using raw Data isn't preferable, preventing the mistake at compile-time and introducing no virtual tables provide better semantics and performance.

Feeling right about it

for some reason, I am feeling like inheritance is a bit "excessive" because there are no functions to be overriden

That's OK. E.g. in some meta code it could even be reasonable to inherit from a class like

template<typename ValueType> struct AliasHelper {
  using value_type = ValueType;
  using reference = ValueType&;
  using pointer = ValueType*;
  using const_reference = ValueType const&;
  using const_pointer = ValueType const*;
};

class Foo: public AliasHelper<int> {};
class Bar: public AliasHelper<short> {};
class Baz: public AliasHelper<long> {};

which doesn't even have fields! However, it greatly reduces boilerplate.

passing_through
  • 1,778
  • 12
  • 24
  • 1
    Re: overhead of inheritance: it's also worth noting that basically anything but virtual method calling has no additional overhead. inheritance is a language construct; when you access `derivedclass.a`, the compiler knows at compile time where `a` is, exactly the same as for the composition case, or the case where you do neither and just copy&paste the same fields in place. – Marcus Müller Nov 09 '21 at 17:14
  • 1
    @MarcusMüller just for completeness, a slight drawback of inheritance of a POD struct compared to having all the fields in the same place is that it does not guarantee the layout of the members and there are no guarantees about padding according to this post: https://stackoverflow.com/questions/22404423/c-pod-struct-inheritance-are-there-any-guarantees-about-the-memory-layout-of. I don't know if it also applies to composition, I don't think the layout part does. Anyway it seems like a very minor issue but since I stumbled upon this thread before asking my question I thought I would mention it. – PrOpoLo Nov 09 '21 at 18:18
  • I think it's not quite that bad, is it? Because it's well defined that the address of that derived object is the address of the first non-static data member, and because we know that downcasting works without changing the address, it directly follows that the first element of the "first" base class needs to come first, right? – Marcus Müller Nov 09 '21 at 18:34
  • No, I don't think it's very important, otherwise I'm guessing there would be a lot of issues with inheritance, not only for PODs. I am no expert on how the memory is layed out but most compilers seem to implement it as you would expect anyway. Your comment just reminded me of this post that's why I linked it, and also for future reference – PrOpoLo Nov 10 '21 at 08:32