3

I had an idea about how to implement interfaces in C. The idea is really simple, but when searching for questions discussing this, I did not see it mentioned.

So, the "standard" inheritance emulation method in C is essentially

struct dad {
   arm_t right_arm;
   arm_t left_arm;
}

struct kid {
   struct dad parent;
   diaper_t diaper;
}

Which I think is ugly, because kid.parent.right_arm just makes less sense than kid.right_arm. There is another way apparently, if you don't mind using gcc-exclusive flags, which is something like

struct kid {
    struct dad;
    diaper_t diaper;
}

but this is not portable. What is portable, however, is

// file: dad.interface
arm_t right_arm;
arm_t left_arm;

// file: kid.h
struct kid {
    #include dad.interface
    diaper_t diaper;
}

For the purposes of this question, I'm not interested in suggestions for alternatives. My question is simply this: what, if anything, is wrong with this approach?

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
user7893856
  • 117
  • 1
  • 7
  • 1
    This is not a good fit for SO as our purpose here is to solve specific problems, not discuss how to shove a square peg into a round hole. The short answer to you question is that Obj-C, C++ and C# were developed for this exact purpose. C has been (fortunately) left alone in regards to these 'enhancements' – KevinDTimm Jun 28 '17 at 15:04
  • 2
    One of the software development skills is to choose the right tool for particular tasks. No one can prohibit you to clean your ears with a screwdriver, but don't be surprised if your broad experience and findings in this area won't be cheered by the community. Simple as that. – iehrlich Jun 28 '17 at 15:04
  • Well, there's a bit more to inheritance in most object-oriented languages than this. Anyway, you can achieve the same effect more cleanlty via by embedding an anonymous structure instance of the base "class", assuming C11 or a decent compiler. Better yet wrap it in anonymous union together with the explicit parent and you can pass direct parent pointers without casting. – doynax Jun 28 '17 at 15:04
  • 1
    As in `struct kid { union { struct dad; struct dad parent; }; diaper_t diaper };` allowing the `dad` member to be omitted while accessing parent fields from the child object, while also forwarding a parent object if desired. A perfectly reasonable an useful little kludge, though naming it inheritance is perhaps a needless distraction. – doynax Jun 28 '17 at 15:11
  • @KevinDTimm Pretty sure this question has very good chances of being answered in a way most people would consider "objective". Again, I wasn't asking HOW to shove a square peg into a round hole, I'm asking WHY the peg is considered square in the first place. – user7893856 Jun 28 '17 at 15:21
  • IMHO I wouldn't recommend trying to emulate inheritance in C by jumping through hoops, instead switch to C++ if you really need inheritance. After all, C++ was invented for that purpose (among others). – AndersK Jun 28 '17 at 15:35
  • 1
    Adding a member in a parent can break a child. Don't do this. – n. m. could be an AI Jun 28 '17 at 15:40
  • @n.m.: While not explicitly stated I should assume that the common include is used to define the parent as well. – doynax Jun 28 '17 at 15:45
  • Also casting from struct kid to struct dad is UB. Upcasting is really fundamental to OO so you can't get around this. – n. m. could be an AI Jun 28 '17 at 16:14
  • @doynax I can infer this much from the design. – n. m. could be an AI Jun 28 '17 at 16:14

2 Answers2

3

Since you pose the question in the context of inheritance, I suppose a more complete (and correct) example would be this:

file: dad.interface

arm_t right_arm;
arm_t left_arm;

file dad.h

struct dad {
    #include "dad.interface"
};

file: kid.h

struct kid {
    #include "dad.interface"
    diaper_t diaper;
};

There are several problems with this, among them:

  1. It's not really inheritance -- it's merely two structures that happen to have matching initial members. struct kid has no other relationship with struct dad, and the former certainly does not inherit anything from the latter. From struct kids perspective, it's not even clear that a struct dad exists.

  2. Although you did not use the term, it is common (albeit unwarranted) for people to assume that polymorphism goes hand-in-hand with inheritance. You don't get that with your approach. Despite sharing a common initial sequence of members, the two structure types are incompatible, and pointers to those types are incompatible, as that term is used by the standard. In particular, accessing a struct kid via a pointer of type struct dad * violates the strict aliasing rule, and therefore produces undefined behavior. (The same does not apply when struct kid embeds a struct dad as its first member.)

  3. The approach has poor extensibility. For example, suppose you want to add struct son and struct daughter inheriting from struct kid. Now you have to pull out struct kid's members into its own separate header, too. I guess you could do that preemptively if you can predict which structure types anyone might ever want to inherit from, or if you just do it to every single structure type. Yikes, what a mess.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • §6.5.2.3 ¶6 says: _One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible. Tw o structures share a common initial sequence if corresponding members have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members._ – Jonathan Leffler Jun 28 '17 at 15:41
  • Since the compiler cannot tell whether two structure types will, or will not, be used in a union in different compilation units (or later in the current one), it means that the layout of structures with a common initial sequence must be the same. – Jonathan Leffler Jun 28 '17 at 15:42
  • @JonathanLeffler Unfortunately this is not the case for their size. `struct a { int a; double b; int c; }` and `struct b { int a; double b; int c; int d; }` have the same size on linux x64 with gcc. This may cause bugs while attempting to use polymorphicaly functions like this `void zero(struct a *p) { memset(p, 0, sizeof(*p)); }` – tgregory Jun 28 '17 at 15:49
  • I said "it means that the layout of structures with a common initial sequence must be the same". I should have said: "it means that the layout of structures with a common initial sequence must be the same for the members in the common initial sequence". There's a caveat elsewhere (§6.2.6.1, footnote 51): 51) _Thus, for example, structure assignment need not copy any padding bits._ Your example is discussing a variation on assignment; my comment applies to member access and layout. Those are different. Your example shows that you can't rely on assignment working — that's a valid caveat. – Jonathan Leffler Jun 28 '17 at 15:58
  • @JonathanLeffler, you are correct *vis a vis* my comments as I first wrote them. I have revised that point in terms of violations of the strict aliasing rule, which I think holds up better. – John Bollinger Jun 28 '17 at 16:00
  • @JohnBollinger: Your edited version is an improvement. Do you want me to remove my commentary? – Jonathan Leffler Jun 28 '17 at 16:04
  • I leave it at your discretion, @JonathanLeffler. Although your comments do not bear directly on the answer in its present form, they still have some relevance to the general topic. – John Bollinger Jun 28 '17 at 16:05
  • @JonathanLeffler I think that this rule about common initial sequence applies only if the union is visible in the current compilation unit. Otherwise compiler can suppose that such a union does not exists. – Marian Jun 28 '17 at 16:36
  • 1
    @Marian: I don't think that's valid. There are rules about equivalent (compatible) types for cross-TU compilations which require the same structure definition to have the same meaning everywhere. – Jonathan Leffler Jun 28 '17 at 16:43
2

You can do it like this:

#define myclass_members int a;\
                void (*f)(void)

struct myclass {
    myclass_members;
};

#define derived_cls_members myclass_members;\
                            double d;\
                            void (*g)(void)

struct derived_cls {
    derived_cls_members;
};

If you are interested, you can check out a whole framework based on this here.

Edit: turns out this approach may work, but as others have pointed out it's dubious. A better option would be to use the anonymous struct expansion added in c11, which can still be used with the preprocessor as @tgregory pointed out.

#define myclass_members struct { int a; void (*f)(void); }

Edit 2: A further look into this reveals that the first approach (with only the preprocessor) should practically work. So if you are not concerned about the mentioned in the comment assignment issues, or the issues that may come up with using functions like memset()/memcpy(), it should be fine.

Sources:

Are C-structs with the same members types guaranteed to have the same layout in memory?

What is the strict aliasing rule?

Edit 3: To not get something optimized away, you'd want to compile with the -fno-strict-aliasing flag if compiling with optimization.

Bottom line: it's dirty, it's a hack, but should work in practice.

Vlad Dinev
  • 432
  • 2
  • 9
  • Yeah that's an alternative, although #include "super.interface" makes the intent a little clearer and you don't have to use annoying backslashes :) – user7893856 Jun 28 '17 at 15:26
  • True, but then you'd have a tone of include files to manage. I find a convention easier to deal with. Neat #include trick, though. – Vlad Dinev Jun 28 '17 at 15:28
  • As @JohnBollinger mentioned padding will be an issue. Quick check with gcc and std=c11 showed that small change might help `#define myclass_members struct { int a; void (*f)(void); }` . – tgregory Jun 28 '17 at 15:49
  • @tgregory Yeah, I have considered the padding problem, but in my experience when the structs get word aligned everything falls in place. Somewhere, in some implementation, it pretty well may not work, idk. Does the anonymous struct expansion somehow ensure the alignment? – Vlad Dinev Jun 28 '17 at 16:01
  • @VladDinev yes. Tested on linux x64 gcc `struct parent_s { parent; };` `struct child_s { parent; int d; }`. If `#define parent int a; double b; int c` `sizeof(parent_s) = 24` `sizeof(child_s) = 24` `offsetof(child_s, d) = 20`. If `#define parent struct { int a; double b; int c; }` `sizeof(parent_s) = 24` `sizeof(child_s) = 32` `offsetof(child_s, d) = 24`. – tgregory Jun 28 '17 at 16:07
  • @tgregory I see. Thank you for the example. In both cases though, you can have a parent_s * point to child_s and it looks like it'll point to a valid parent_s struct, regardless of the exact padding because the parent_s members are first anyway. Is that so or am I missing something? – Vlad Dinev Jun 28 '17 at 16:20
  • You won't be able to do polymorphic block copies using functions such as `memcpy` since this will overwrite members which happened to fall into padding (remember `sizeof(parent_s)` returns more memory than actualy belong to an embedded `parent_s`. – tgregory Jun 28 '17 at 16:34
  • @tgregory Got that. Thank you for the clarification. – Vlad Dinev Jun 28 '17 at 17:32