Struct Inheritance in C++

Is multiple-level struct inheritance guaranteed to work everywhere?

That the kind of "multi-level inheritance" you describe must work follows from the same principles -- explained in the other Q&A you referenced -- that makes this kind of inheritance work at all. Specifically, the standard explicitly provides that casting the addresses of structures and of their initial members between the applicable types has the desired effect:

A pointer to a structure object, suitably
converted, points to its initial member [...] and vice versa.

(paragraph 6.7.2.1/15)

So consider this declaration, relative to the structure definitions provided:

C c;

The quoted provision specifies that &c == (C *) &c.base and (B *) &c == &c.base are both true.

But c.base is a B, so the provision also specifies that (A *) &c.base == &c.base.base and &c.base == (B *) &c.base.base are both true.

Since (B *) &c == &c.base is true and &c.base == (B *) &c.base.base are both true, it follows that (B *) &c == (B *) &c.base.base is also true.

Casting both sides to either A * or C * then produces also the equalities (A *) &c == &c.base.base and &c == (C *) &c.base.base.

This reasoning can be extended to an arbitrary nesting depth.

One can quibble a bit about dynamically allocated structures vis a vis the strict aliasing rule, but there's no reason to think that it is supposed to work any differently in that case, and as long as one first accesses the dynamically-allocated space via an lvalue of the most specific type (C in this example), I see no scenario that supports a different interpretation of the standard for the dynamic-allocation case than applies to other cases. In practice, I do not expect initial access via the most specific type actually to be required by any implementation.

C++ struct inheritance

classes have their members private by default and structs have theirs public by default.

Structs also inherit public by default, where classes inherit private by default.

Even if the language allows it, is it a good practice to inherits structs ?

Sure. It works exactly as you would expect.

Is it possible to prevent a struct to declare a virtual method, which will create a vtable and modify the size of the struct ?

Not yet. There is a proposal for C++20+ (P0707) to allow exactly this, but it's still pretty young and not implemented far enough to be used anywhere. In particular, search for "3.6 plain_struct" to see how they enforce plain structs to be that.

In general I would recommend using a struct when you're using it as a "struct" sort of function - holding data without invariants. If you have invariants, you should keep them using encapsulation and data hiding, so it should be a class.

C++: Can a struct inherit from a class?

Yes, struct can inherit from class in C++.

In C++, classes and struct are the same except for their default behaviour with regards to inheritance and access levels of members.

C++ class

  • Default Inheritance = private
  • Default Access Level for Member Variables and Functions = private

C++ struct

  • Default Inheritance = public
  • Default Access Level for Member Variables and Functions = public

typedef struct inheritance in C

Put the common superclass as an initial member of each subclass.

typedef struct Car {
Item item;
char *make;
} Car;

typedef struct Book {
Item item;
char *title;
char *author;
} Book;

You can then cast a Book* or Car* to Item* when calling generic Item functions.

Another option is a discriminated union.

typedef struct Item {
// general Item stuff goes here
enum {Car, Book} type;
union {
Car car;
Book book;
};
} Item;

But if you need to do a lot of this, maybe you should use C++ instead of C, so you have real class hierarchies.

How to inherit structure in a plain C

I know that it is possible to typecast and offset the pointer

Not necessarily:

void foo(Kind1*);
struct Kind2
{
Kind1 basedata;
int z;
float angle2;
float angle3;
}

//...
Kind2 k;
foo(&(k.basedata));

How does struct inheritance not violate the strict aliasing rule?

A. It seems to me this technique breaks the strict aliasing rule. Am I wrong, and if so, why?

Yes, you are wrong. I'll consider two cases:

Case 1: The C is fully initialized

That would be this, for example:

C *c = malloc(sizeof(*c));
*c = (C){0}; // or equivalently, "*c = (C){{{0}}}" to satisfy overzealous compilers

In that case, all the bytes of the representation of a C are set, and the effective type of the object comprising those bytes is C. This comes from paragraph 6.5/6 of the standard:

If a value is stored into an object having no declared type through an
lvalue having a type that is not a character type, then the type of
the lvalue becomes the effective type of the object for that access
and for subsequent accesses that do not modify the stored value.

But structure and array types are aggregate types, which means that objects of such types contain other objects within them. In particular, each C contains a B identified as its member base. Because the allocated object is, at this point, effectively a C, it contains a sub-object that is effectively a B. One syntax for an lvalue referring to that B is c->base. The type of that expression is B, so it is consistent with the strict-aliasing rule to use it to access the B to which it refers. That has to be ok, else structures (and arrays) would not work at all, whether dynamically allocated or not.*

But, as discussed in my answer to your previous question, (B *)c is guaranteed to be equal (in value and type) to &c->base. Thus *(B *)c is another lvalue referring to the B that is the first member of *c. That the syntax of that expression is different from that of the previous lvalue we considered is of no account. It is an lvalue of type B, associated with an object of type B, so using it to access the object to which it refers is one of the cases allowed by the SAR.

None of this is any different from the statically and automatically allocated cases.

Case 2: The C is not fully initialized

That could be something like this:

C *c = malloc(sizeof(*c));
*(B *)c = (B){0};

We have thereby assigned to the initial B-sized portion of the allocated object via an lvalue of type B, so the effective type of that initial portion is B. The allocated space does not at this point contain an object of (effective) type C. We can access the B and its members, read or write, via any acceptably-typed lvalues referring to them, as discussed above. But we have a strict aliasing violation if we

  • attempt to read *c as a whole (e.g. C c2 = *c;);
  • attempt to read C members other than base (e.g. X x = c->another;); or
  • attempt to read the allocated object via an lvalue of most unrelated types (e.g. Unrelated_but_not_char u = *(Unrelated_but_not_char *) c;

The first two of those cases are of interest here, and they make sense in terms of the dynamically allocated object, when interpreted as a C, not being fully initialized. Similar incomplete-initialization cases can arise with automatically allocated objects, too; they also produce undefined behavior, but by different rules.

Note well, however, that there is no strict aliasing violation for any write to the allocated space, because any such write will (re)assign the effective type of (at least) the region that is written to.

And that brings us to the main tricksome bit. What if we do this:

C *c = malloc(sizeof(*c));
c->base = (B){0};

? Or this:

C *c = malloc(sizeof(*c));
c->another = 0;

The allocated object does not have any effective type before the first write to it (and in particular, it does not have effective type C), so do write-to-member expressions via *c even make sense? Are they well-defined? The letter of the standard might support an argument that they do not, but no implementation adopts such interpretation, and there is no reason to think that any ever would.

The interpretation most consistent with both the letter of the standard and universal practice is that writing through a member-access lvalue constitutes simultaneously writing to the member and to its host aggregate, thus setting the effective type of the whole region, even though only one member's value is written. Of course, that still does not make it ok to read members whose values have not been written -- because their values are indeterminate, not because of the SAR.

That leaves this case:

C *c = malloc(sizeof(*c));
*(B *)c = (B){0};
B b2 = c->base; // What about this?

That is, if the effective type of an initial region of the allocated space is B, can we use a member-access lvalue based on type C to read the stored value of that B region? Again, one might argue not, on the basis that there is no actual C, but in practice, no implementation makes that interpretation. The effective type of the object being read -- the initial region of the allocated space -- is the same as the type of the lvalue used for access, so in that sense there is no SAR violation. That the host C is wholly hypothetical is a question primarily of syntax, not semantics, because the same region can definitely be read as an object of the same type via an alternative expression.


* But the SAR nevertheless forestalls any debate on this point by providing that "an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union)" is among the types that may be accessed. This clears any ambiguity surrounding the position that accessing a member also constitutes accessing any objects containing it.



Related Topics



Leave a reply



Submit