Reputation: 903
Let's suppose you are writing a C struct
which represents a course in a meal. One of the fields in the course struct
is of type:
enum TP_course {STARTER, MAINCOURSE, DESSERT};
Then, depending on the type of the course, you have a subtype:
enum TP_starter {SALAD, GRILLEDVEGETABLES, PASTA};
enum TP_maincourse {BEEF, LAMB, FISH};
enum TP_dessert {APPLEPIE, ICECREAM, MOUSSE};
Given that only one of such enums will be used at a time (depending on the type of the course), it makes sense to aggregate them in a union
:
union U_subtype {
enum TP_starter s;
enum TP_maincourse m;
enum TP_dessert d;
};
So the course struct
would look like this:
struct S_course {
enum TP_course type;
union U_subtype stype;
float price_in_USD;
int availability;
...and all the rest of data would follow...
};
Ok, everything is clear, but... is there any coding strategy I could follow to try to enforce safe access to the stype
tagged union above? Perhaps making it opaque in some way?
For example, if I write a switch/case
block for an enum
and I forget to write a case
for a value, the compiler will trigger a warning, which is of great help for maintaining the code in the future. But if I access stype.s
without first checking if type==STARTER
, the compiler cannot be smart enough for realizing of the risky coding, and won't warn at all.
Can I organize the code in some way so that it's not possible to access the members of the U_subtype
union except in a very limited place where I clearly document how the access to such members must be done?
Upvotes: 9
Views: 16229
Reputation: 903
After thinking a lot about this, I chose an approach which could be considered as a fourth option in addition to the other three suggested by PSkocik: To redesign the struct
so that there's no type and subtype, but just subtype. Then the type isn't provided by the struct
, but by a helper function.
Something like this:
enum TP_course {STARTER, MAINCOURSE, DESSERT};
enum TP_subtype {SALAD, GRILLEDVEGETABLES, PASTA,
BEEF, LAMB, FISH, APPLEPIE, ICECREAM, MOUSSE};
struct S_course {
enum TP_subtype stype;
float price_in_USD;
int availability;
/*...*/
};
enum TP_course getCourse(struct S_course *c) {
switch(c->stype) {
case SALAD:
case GRILLEDVEGETABLES:
case PASTA:
return STARTER;
case BEEF:
case LAMB:
case FISH:
return MAINCOURSE;
case APPLEPIE:
case ICE-CREAM:
case MOUSSE:
return DESSERT;
}
}
This design guarantees safe read/write access to the type of the struct
. It prevents you from leaving the struct
in undefined behavior (for example, setting the type to STARTER but forgetting to set the subtype accordingly), and it also prevents you from reading (and writing) a union
member that it's not the current one.
I tend to prefer this style of designs, and I admit I got this influence from the Apple UI guidelines: create a design that prevents the user from entering unsupported/undefined data; never spread data in different places when it can be in one single place; avoid absurd/illegal data status from the design so that you don't need to check if data is legal: it always is; avoid special cases whenever you can do the same with a single general case; etc, etc, etc...
Upvotes: 5
Reputation: 60127
You can
.
/* header */
struct S_course; //forward declaration
enum TP_starter {SALAD, GRILLEDVEGETABLES, PASTA};
enum TP_maincourse {BEEF, LAMB, FISH};
enum TP_dessert {APPLEPIE, ICECREAM, MOUSSE};
void S_course__set_starter(struct S_course *this, enum TP_starter starter);
//accessor functions
void S_course__set_maincourse(struct S_course *this, enum TP_maincourse maincourse);
void S_course__set_dessert(struct S_course *this, enum TP_dessert dessert);
/* c file */
enum TP_course {STARTER, MAINCOURSE, DESSERT};
union U_subtype {
enum TP_starter s;
enum TP_maincourse m;
enum TP_dessert d;
};
struct S_course {
enum TP_course type;
union U_subtype stype;
float price_in_USD;
int availability;
/*...*/
};
void S_course__set_starter(struct S_course *this, enum TP_starter starter)
{
this->type = STARTER;
this->stype.s = starter;
}
Use member names that scream don't touch me, or a name like tagged_union
, which should make it obvious how it needs to be accessed.
or
Switch to C++ and use its access control features (private/protected) to hide only some members while allowing access through public member/friend functions
Upvotes: 4