3

I have structs that just contain data, a minimal example would be

struct A{
    int a;
};

struct B{
    int a;
    int b;
};

I realized now after using these two structs that A is always should be a subset of B. Because of this, I would like to couple the structs together, so any change in A is reflected in B.

The most obvious way to do this is with inheritance, so just

struct B: public A{
    int b;
};

However, I don't want any accidental object slicing, so I also want for B to not visibly subtype A. Basically, I don't want users to know that B extends A, it should be hidden as an implementation detail.

I know that the standard is "favor composition over inheritance", but that would make it so accessing the fields of B would be like "B.inner.a", which I don't really want for various reasons. Like I stated before, I want the knowledge that B is a superset of A to be an implementation detail.

So far, the only things I know about C++ inheritance is that it can be public, protected, or private, but none of those modifiers help me here. Is there any language features or standard practices that can help me here?

k huang
  • 409
  • 3
  • 10

2 Answers2

1

Explicitly deleting the appropriate constructor and assignment operator will be sufficient if the goal is to prevent object slicing.

struct B;

struct A{
    int a=0;

    A();

    A(const B &)=delete;

    A &operator=(const B &)=delete;
};

struct B : A {
    int b=0;
};



B b1, b2;

A a1, a2;

void foo()
{
    b1=b2; // Ok.
    a1=a2; // Ok.

    a1=b1; // Error.

    B b3{b2}; // Ok.
    A a3{a2}; // Ok.

    A b4{b1}; // Error
}

The only downside is loss of convenient aggregate initialization, that will need to be remedied with explicit constructors.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • Thanks, this looks quite promising! Would you mind elaborating on what "convenient aggregate initialization" is? – k huang Oct 20 '22 at 01:05
  • 1
    See https://en.cppreference.com/w/cpp/language/aggregate_initialization or your favorite C++ textbook. – Sam Varshavchik Oct 20 '22 at 01:22
  • To extend this approach more generally, assuming the OP is doing a lot of this, the logic can be wrapped up in a macro and use template type resolution to do the type checking: https://godbolt.org/z/r1rWhMT6s – paddy Oct 20 '22 at 01:27
1

You can use private inheritance, but then make A::a public:

struct B : private A {
    using A::a;
    int b;
};

(With the appropriate constructors added since B{1, 2} will no longer work without it)

Artyer
  • 31,034
  • 3
  • 47
  • 75
  • I like it. But would I have to do using A::blah blah for every field? Because my actual struct has more than 1 field. – k huang Oct 20 '22 at 03:00
  • 1
    You would, but you can put it one statement `using A::a, A::b, A::c, etc;` – Artyer Oct 20 '22 at 03:01