A new (hopefully) daily review on C++
.
This is another new series, which I plan to update daily (well, hopefully :P). It’s mainly about C++
, which is my current main development language. Each day I’ll follow 1 item in the Effective C++ 2nd edition by Scott Meyers to discuss 1 tiny point of C++
.
The item 1 of the book tells us when we should prefer the compiler to the preprocessor, or more specifically, why we should prefer const
s, enum
s and inline
s to #define
. Now let’s take a closer look.
Prefer const
to #define
There are two ways we may consider, when we want to define a constant:
|
|
However, since PI_1
may be removed by preprocessor and never be seen by compilers, making it absent in the symbol table, this can be confusing during debugging if we get an error refering to 3.1415926 rather than PI_1
(especially when PI_1
is defined in a header file written by somebody else). Thus, instead of using a preprocessor macro, we’d better go with const
. Below is some tricks with const
.
Tricks on const
definition
-
When defining constant pointers, use two
const
to make sure both the pointer as well as the content pointed by the pointer are immutable:1
const char * const blogger = "Nzo";
-
For class-specific constants, of which we want to limit the scope, we declare it as a
static
member, since there’s no way we may create a class-scope-specific or class-encapsulated(i.e., private) constant using a#define
(once a macro is defined, it’s in force for the rest of the compilation unless it’s#undefed
somewhere along the line). Pay attention to the difference between constant declaration and constant definition:1 2 3 4 5 6 7
// game.h class GamePlayer { private: static const int NUM_TURNS = 5; // constant declaration with initial value static const double PI; // constant declaration without initial value int scores[NUM_TURNS]; // use of constant };
1 2 3
// game.cpp const int GamePlayer::NUM_TURNS; // constant definition in impl. file const double GamePlayer::PI = 3.14; // provide initial value at definition
It’s worth noting that older compilers (primarily those written before 1995) may complain about providing initial value for static class member at the point of declaration. Most of time we may solve the problem by putting the initial value at definition time, but in the situation where we need the integral value of NUM_TURNS during compilation time (in the example above, compilers must know the size of the array during compilation), we may use a trick affectionately known as the enum hack, which takes advantage of the fact that the value of an enumerated type can be used where
int
s are expected:1 2 3 4 5 6
// Game.h class GamePlayer { private: enum { NUM_TURNS = 5 }; // NUM_TURNS is a symbolic name for 5 int scores[NUM_TURNS]; // fine with old compilers };
There are mainly 2 reasons we may still see the enum hack today:
- It’s legal to take the address of a
const
, but it’s not legal to take the address of an enum, so if you don’t want people to get a pointer or reference to your integral constants, an enum will enforce that constraint (and this makes the enum behave somewhat like a #define). - It’s purely pragmatic. Lots of code (such as template metaprogramming, item 48) still employs it.
For modern compilers, on the other hand, rules change a bit in a more convenient way. Usually C++ requires that you provide a difinition for anything you use, but class-specific
constant
s that arestitic
and ofintegral type
(e.g., integers, chars, bools) are an exception. As long as you don’t take thier address, you can declare and read them without providing a definition, and (good) compilers will not allocate unneccessary memory for these integral-type const objects, which will make the static const integral an lvalue. - It’s legal to take the address of a
Prefer inline
to #define
Another common (mis)use of the #define
is using it to implement macros that look like functions:
|
|
This macro will lead to following weird things:
|
|
Thus, we may prefer using a regular inline function that provides both predictable behavior and type-safety:
|
|
If you complain that the above inline function only deals with int
type, then we may just use inline template function, which nicely fixes the problem:
|
|
Basically, this template generates a whole family of functions, each of which takes two objects convertible to the same type and returns a reference to the greater of the two objects in const version.
When to use preprocessor
However, preprocessor is never dead. We still need preprocessor for tasks such as #include
to include libraries, as well as #ifdef
/#ifndef
to control compilation.