If the size doesn't have to be one byte, we can (ab)use pointers:
typedef struct foo *rolloverdetection;
typedef struct bar *rolloverdetected;
#define ROLLOVERDETECTION_OFF ((rolloverdetection) 0)
#define ROLLOVERDETECTION_ON ((rolloverdetection) 1)
#define ROLLOVERDETECTED_NO ((rolloverdetected) 0)
#define ROLLOVERDETECTED_YES ((rolloverdetected) 1)
#define ROLLOVERDETECTED_UNKNOWN ((rolloverdetected) 2)
The preprocessor symbols aren't constant expressions any more and we can't use them as switch labels and whatnot.
A good solution to this is to use C++ type-safe enums
. This is one of the advantages of writing your code in "Clean C": an informal name given to working in a language dialect which compiles as some version of C, as well as some version of C++ with the same behavior.
Simply:
typedef enum {
ROLLOVERDETECTION_OFF,
ROLLOVERDETECTION_ON
} rolloverdetection;
typedef enum {
ROLLOVERDETECTED_NO,
ROLLOVERDETECTED_YES,
ROLLOVERDETECTED_UNKNOWN = 255
} rolloverdetected;
In C, you can still assign ROLLOVERDETECTED_YES
to a variable of type rolloverdetection
, but not so in C++.
If you keep the code compiling as C++, you can use a C++ compiler to check for these violations, even if the shipping build of the code doesn't use C++.
If storing the value in 8 bits is important, I seem to recall GCC supports enum-typed bitfields as an extension (not in ISO C):
struct whatever {
rolloverdetected roll_detect : 8;
};
C++ enums are not perfectly type safe; implicit conversion from an enum
member to an integer type is possible:
int roll = ROLLOVERDETECTION_ON;
but not in the reverse direction.
By the way, other techniques open up if you compile as C and C++, like being able to use more nuanced type casts:
#ifdef __cplusplus
#define strip_qual(TYPE, EXPR) (const_cast<TYPE>(EXPR))
#define convert(TYPE, EXPR) (static_cast<TYPE>(EXPR))
#define coerce(TYPE, EXPR) (reinterpret_cast<TYPE>(EXPR))
#else
#define strip_qual(TYPE, EXPR) ((TYPE) (EXPR))
#define convert(TYPE, EXPR) ((TYPE) (EXPR))
#define coerce(TYPE, EXPR) ((TYPE) (EXPR))
#endif
So now, for instance, we can do
strip_qual(char *, str)
In C, this is just an unsafe (char *) str
cast. In C++, the above macros produce const_cast<char *>(str)
. So if str
starts out being const char *
, but then someone changes it to const wchar_t *
, the C++ compiler will diagnose the above cast. Yet, our project doesn't require a C++ compiler in order to build.
In conjunction with this, if you're using GCC, its C++ front end has -Wold-style-cast
that will find all the places in the code where you're using the (type) value
cast notation.