Introduction
I recently had to review some c++ code written by someone else. The code was well written. It checked for error conditions and did reasonable things if something went wrong. But there was something about it that irked me.
For obvious reasons I'm not showing the real classes. Instead I'll illustrate the irksome thing with contrived examples.
The Original Class
The class I was reviewing ran something like this.
class CAClass
{
public:
static const UINT aConstantOfInterestToThisClass1 = 0;
static const UINT aConstantOfInterestToThisClass2 = 1;
CAClass(UINT constValue, LPCTSTR someOtherParameter);
void DoSomething();
private:
UINT m_const;
CString m_parameter;
};
The class CAClass
constructor takes a parameter that specifies something of interest to the class, constValue
and a pointer to a string. DoSomething()
does something relevant based on the parameters passed to the constructor. The class implementation was coded something like this.
CAClass::CAClass(UINT constValue, LPCTSTR someOtherParameter)
{
m_const = constValue;
m_parameter = someOtherParameter;
}
void CAClass::DoSomething()
{
switch (m_const)
{
case aConstantOfInterestToThisClass1:
break;
case aConstantOfInterestToThisClass2:
break;
default:
RaiseException(ERROR);
break;
}
}
This code will work fine. So what's wrong with it?
How I'd have coded it
class CBClass
{
public:
enum constsOfInterestToThisClass
{
bConstantOfInterestToThisClass1 = 0,
bConstantOfInterestToThisClass2,
};
CBClass(constsOfInterestToThisClass constValue,
LPCTSTR someOtherParameter);
void DoSomething();
private:
constsOfInterestToThisClass m_const;
CString m_parameter;
};
and the implementation...
CBClass::CBClass(constsOfInterestToThisClass constValue,
LPCTSTR someOtherParameter)
{
m_const = constValue;
m_parameter = someOtherParameter;
}
void CBClass::DoSomething()
{
switch (m_const)
{
case bConstantOfInterestToThisClass1:
break;
case bConstantOfInterestToThisClass2:
break;
default:
RaiseException(ERROR);
break;
}
}
There's almost no difference. Any good c++ compiler would compile identical code for both classes.
So what's the difference?
The first class,
CAClass
, defines a bunch of
const
ant values of interest to itself.
CBClass
defines the same named constants but it does it as an
enum
. An
enum
defines a limited set of valid values and can also be used as a pseudo datatype. Look at the difference in the definitions of the constructors.
CAClass(UINT constValue, LPCTSTR someOtherParameter);
CBClass(constsOfInterestToThisClass constValue, LPCTSTR someOtherParameter);
CAClass
can accept any valid UINT
value. That's over 4 billion possible values, only 2 of which are of any possible interest to the class. Any valid UINT
value outside of 0 or 1 will cause the DoSomething
function to raise an exception that other code within your application must handle.
CBClass
in contrast will accept only one of the two enum
values. Try and pass any invalid const
ant and the compiler will (should) complain with an error or warning message.
Contrast this
CAClass obj(1000, _T("This is a string"));
obj.DoSomething();
with this
CBClass obj(1000, _T("This is a string"));
obj.DoSomething();
The first example CAClass obj(1000, _T("This is a string"));
will compile and throw an exception at runtime when it calls obj.DoSomething()
. The second example CBClass obj(1000, _T("This is a string));
will at the very least throw up an error message in your compiler, at compile time. A good implementation will fail to produce an executable file until you've corrected the error and provided a valid value. VC++ flags a warning but produces an executable if you've set error level to 3 and not checked 'warnings as errors'. I always compile my code at error level 4 and 'warnings as errors'.
The CBClass
constructor expects a first parameter of type constsOfInterestToThisClass
. This may be either bConstantOfInterestToThisClass1
or bConstantOfInterestToThisClass2
or a variable of type constsOfInterestToThisClass
. The compiler will let you define a variable of type constsOfInterestToThisClass
but will only let you assign values from the enum
values you define.
CBClass::constsOfInterestToThisClass var;
var = CBClass::bConstantOfInterestToThisClass1;
var = 47;
Another issue
From reading the foregoing it's tempting to conclude that the final default:
case in CBClass::DoSomething()
is superfluous. You might even have thought I left it in by mistake. After all, if you've used an enum
correctly the default:
should never occur. That's true today. But what if you add a new enum
constant sometime down the track and forget to add code to the DoSomething()
function to handle it? If your switch statement silently ignores enum
values it doesn't know about you run the risk of incurring all kinds of unexpected (and difficult to trace) behaviour. Leaving the default:
case in place greatly increases your chances of catching such oversights during program development and testing.
Casts
As one or two readers have pointed out it's possible to defeat the whole point of this article by using casts. For example one could code my error example from above thusly:
CBClass::constsOfInterestToThisClass var;
var = CBClass::bConstantOfInterestToThisClass1;
var = (CBClass::constsOfInterestToThisClass) 47;
and the compiler will happily compile your code. Of course it won't run as you expected but if you left the default:
code in the switch
statement at least you'll get an exception at runtime and hopefully during program testing. What you're doing here of course is saying to the compiler, in effect, 'you know and I know that 47 isn't a valid constant here but I know better than you do so just go ahead and compile it for me'. Once you've asserted your superior knowledge to the compiler all bets are off.
Interestingly, the compiler considers some casts to be so extreme that it still won't compile them without an intermediate step. Ie, cast something to something else, then cast that something else to the final type.
Conclusion
The compiler will do a lot of error checking for you at compile time, if you let it. Using
enum
's rather than
const
's helps the compiler find places in your code where you've made incorrect assumptions. The compiler's a lot more thorough than most of us are when it comes to checking datatypes!