|
Compile-Time Asserts
Submitted by |
A compile-time assert (CTA) is basically a little trick you can do to force
the compiler to generate errors during compilation based on conditions you
supply. There are many times where you'll want a CTA instead of a run-time
assert (RTA). CTA's are especially useful in a team environment, as often
one programmer will change something that breaks something in a different
area, but since often one doesn't have the time to test every single piece
of functionality in an application before checking code in, these bugs can
go untracked for a long time. Obviously in order to track all bugs of this
type, you really need a full-time Quality Assurance team working during the
entire life-cycle of the product. However, the earlier you catch bugs the
better, and CTA's force you or others to reevaluate your code by preventing
compilation.
CTA's are obviously only able to evaluate compile-time conditions, so RTA's
still have their place. For instance, a CTA could not assert the value of a
non-const variable. However, you can use CTA's to enforce things like
class/struct size, template parameters, the value of const variables, and
other types of information available at compile time.
Ok, so much for the introduction. The code for a simple CASSERT macro looks
like this:
#define CASSERT(expn) typedef char __C_ASSERT__[(expn)?1:-1] |
So if "expn" evaluates to true, then the macro expands to:
typedef char __C_ASSERT__[1]; |
Which the compiler accepts silently. However, if the expression evaluates to
false, the macro expands to:
typedef char __C_ASSERT__[-1]; |
Which the compiler will flag as an error, because negative subscripts are
not valid.
Say you have a class that, for whatever reason, needs to occupy a specific
number of bytes in memory (obviously this sort of thing should be avoided,
but every once in a while it's necessary). You can do the following with the
CASSERT macro:
CASSERT(sizeof(MyClass) == 64); |
If the size of MyClass changes, we will get the following error during
compile-time (MS VC6):
error C2118: negative subscript or subscript is too large
We now successfully have enforced the size of a data type during
compile-time! This is great, but the error message could be a tad more
useful, don't you think? IMHO, this macro deals with most situations just
fine as you can tell the people you work with how CASSERT works, and comment
your asserts as to why they would assert (you should do this anyway). But
there are certain circumstances where you want to give people more friendly
errors. While there isn't a huge amount of flexibility in this area, you can
get some control. The following is an example of such - this is off the top
of my head, so there might be a better way:
#define CASSERT_MSG(exp, msg) \
{ \
struct cassert_t \
{ \
template<bool cond \
struct checkCondition {}; \
\
template< \
struct checkCondition<true { enum { CASSERT_##msg }; }; \
}; \
enum { CASSERT_##msg }; \
cassert_t::checkCondition<exp::CASSERT_##msg; \
} |
This defines a somewhat more powerful version of CASSERT that takes the
error message as an argument. First of all, the error message must be a
single identifier, so you have to use things like "sizeof_MyClass_is_not_64"
rather than "sizeof MyClass is not 64!!". Secondly, it won't be exactly the
error message the offender will see, but it will appear within the error
message.
The way it works is by leveraging template specialization. The default
definition of the checkCondition template struct is empty. However, a
specialization of the template is defined for the value "true". Inside of
this specialization, an enum exists with the value of "CASSERT_" appended
with the error message you want. The line
"cassert_struct::checkCondition::CASSERT_##msg" attempts to access the
enum inside of the template instance, where the template instantiated
depends on whether the expression passed in is true. (FYI, the "enum {
CASSERT_##msg };" on the second-to-last line is just a workaround for VC6 so
that it only throws one error. This probably won't be needed for other
compilers.)
For instance, consider the previous CTA, now using the new CASSERT_MSG
macro:
CASSERT_MSG(sizeof(MyClass)==64, sizeof_MyClass_is_not_64); |
The error message the offender will see (again, in VC6):
error C2039: 'CASSERT_sizeof_MyClass_is_not_64' is not a member of
'cassert_struct::checkCondition'
This gives a much more clear indication to the offender as to what is
happing.
As a final note, I'll cover compile-time type assertions, as this was
something that was brought up by someone for my previous TOTD.
#define CASSERT_ISTYPEOF(targ, class) \
(void) static_cast<class*((targ*)0) |
There is really nothing fancy going on here. All we are doing is forcing the
compiler to try to cast a targ* to a class* using static_cast. If it fails,
then we know that the class and targ datatypes are unrelated.
For instance, consider the following:
class A {};
class B : public A {};
class C {};
CASSERT_ISTYPEOF(B, A);
CASSERT_ISTYPEOF(C, A); // error! |
The error message received in VC6 is as follows:
error C2440: 'static_cast': cannot covert from 'C*' to 'A*'.
This approach is useful in some template methods, where you want to make
sure that the template arguments have a specific base class (occassionaly
this is desired). If you want specific error messages for type asserts there
are tricks you can do, but I'll leave it up as an exercise for the reader.
Compile-time asserts will definitely help you out, whether you're working
alone or with a team. The biggest obstacle is learning when to use them.
Together with runtime asserts, you can write a very stable codebase that
will be more manageable and maintainable.
- Dan Ogles
|
The zip file viewer built into the Developer Toolbox made use
of the zlib library, as well as the zlibdll source additions.
|