C / C ++ allows you to perform tests of constant expressions at the compilation of the program. This is a cheap way to avoid problems when modifying code in the future.
I will review the work with:
- transfers (enum),
- arrays (their synchronization with enum),
- switch constructions
- as well as working with classes containing heterogeneous data.
BOOST_STATIC_ASSERT and all-all-all
There are
many ways to break a compiler at compile time. Of these, I like this performance the most:
#define ASSERT(cond) typedef int foo[(cond) ? 1 : -1]
But if you use boost in your program, you don’t need to invent anything:
BOOST_STATIC_ASSERT . Support also
promises to be in C ++ 11 (static_assert).
')
With the tool sorted out, now about use.
Control the number of elements in enum
Enumerations - a set of related constants, which, as a rule, are used at the branch point of program logic. Branching points are usually several, and you can easily miss something.
Example:
enum TEncryptMode { EM_None = 0, EM_AES128, EM_AES256, <b>EM_ItemsCount</b> };
The last element is not an algorithm, but an auxiliary constant numbered one greater than the maximum semantic element.
Now, everywhere, where constants from this set are used, you just need to add a check:
ASSERT(EM_ItemsCount == 3);
If new constants are added in the future, the code in this place will cease to be compiled. This means that the author of the changes will have to look at this section of the code and, if necessary, take into account the new constant.
As a bonus from the introduction of EM_ItemsCount, it is possible to insert runtime checks of function parameters:
assert( 0 <= mode && mode < EM_ItemsCount );
Compare with the option without such a constant:
assert( 0 <= mode && mode <= EM_AES256 );
(add EM_AES512 and get the wrong check)
Arrays and enum
A special case of verification from the previous section.
Suppose we have an array with parameters to the same encryption algorithms (an example is a little sucked from the finger, but in life there are similar cases):
static const ParamStruct params[] = { { EM_None, 0, ... }, { EM_AES128, 128, ... }, { EM_AES256, 256, ... }, { -1, 0, ... } };
This structure needs to be kept in sync with TEncryptMode.
(Why do I need the last element of the array, I think, no need to explain.)
We will need an auxiliary macro to calculate the length of the array:
#define lengthof(x) (sizeof(x) / sizeof((x)[0]))
Now, you can write a check (it is better if you immediately follow the definition of params):
ASSERT( lengthof(params) == EM_ItemsCount + 1 );
upd: In the
comments haborrower
skor offered a more secure version of the macro lengthof, for which he thanks.
switch
Everything is obvious (after the examples above). Before switch (mode) add:
ASSERT(EM_ItemsCount == 3);
A slightly less obvious runtime check:
ASSERT(EM_ItemsCount == 3); switch( mode ) { case ...: ... break; ... <b>default: assert( false );</b> }
Additional bastion for defense against errors. If actions are processed in the same way, it is better to list several case-conditions for one action, leaving default not to be occupied:
... case ET_AES128: case ET_AES256: ... break; ...
Classes with disparate data
Take a break from enum and look at this class:
class MyData { ... private: int a; double b; ... };
It may well be that sometime in the future, someone will want to add the int c variable to it. The class by this time became big and difficult. How to find the points at which you need to register the variable c?
We propose such a semi-automatic solution: we set up a data version constant in the class:
class MyData { static const int DataVersion = 0; ... };
Now, in all methods in which it is important to track the integrity of all data, you can write:
ASSERT(DataVersion == 0);
Adding new data to the class will have to manually increase the DataVersion constant (discipline is required here, alas). But the compiler will immediately pay attention to those places that need to be checked. These points of verification should include:
- constructors
- assignment operator (operator =)
- comparison operators (==, <, etc),
- read / write data (including <<, >>),
- destructor (if it is not trivial).
The remaining places of the check depend on the internal logic (output to the log, for example).
The same constant (DataVersion) is convenient to use when saving data to disk (if interested, I can write about it separately).
Benefit
What is the result?
Pros:
- Automatic integrity check at compile time (sometimes it saves hours and even debugging days).
- Zero overhead at run time.
Minuses:
- Additional code (albeit relatively small).
- Load on self-discipline (you just need to view the crashed drops, and not just fix the constant).
For me, the advantages outweigh, and for you?
upd Added code highlighting.