Tuesday 10 January 2012

Prefer compile-time to run-time value range checks: use enumerations

In a perfect world, compiler should be able to detect all errors in the code. If types mismatch or value is out of the valid range - compiler should complain. How can we help compiler to achieve this? One answer is: by defining our custom types.

Enumerations

When we define enumeration, we are introducing a new type. Let's say some integer variable can be assigned only certain values, from a predefined set. Function that assigns value to a variable could look like this:



Ok, what happens if someone writes



This code compiles fine but it sets variable to a value out of the valid range and this can lead to some logical errors.

How can we improve this code? Obviously, we can check whether value is valid. If it is not, we can either throw exception, set variable to some default value or just quietly return from a function:



Each time this function is called it will use CPU resources to perform value check. But is it necessary? Can we make this function more optimal in a run-time? Yes, we can: by moving value check from run-rime to compile time! If we define our custom type - enumeration - which defines valid range, we will be able to use compiler to find all places where invalid value is passed:



SetLevel(2) will cause compiler to report an error.

One example of the real-life usage of this approach could be writing a wrapper around Windows Thread API. E.g. SetThreadPriority has an argument nPriority which is of type int and so can accept any integer value despite the fact that valid values are from a predefined set (THREAD_PRIORITY_ABOVE_NORMAL = 1, THREAD_PRIORITY_BELOW_NORMAL = -1, THREAD_PRIORITY_HIGHEST = 2, ...). Passing invalid integer values can be prevented by introducing our enumeration which enumerates all valid values for thread priority:



NOTE: enum value is implicitly casted to int but int value must be explicitly casted to enum type.

2 comments:

Stefan Naewe said...

I don't know which compiler you're using but mine says:

g++ level.cpp -o level
level.cpp: In function ‘int main()’:
level.cpp:18: error: invalid conversion from ‘int’ to ‘Level’
level.cpp:18: error: initializing argument 1 of ‘void SetLevel(Level)’
level.cpp:19: error: invalid conversion from ‘int’ to ‘Level’
level.cpp:19: error: initializing argument 1 of ‘void SetLevel(Level)’
level.cpp:20: error: invalid conversion from ‘int’ to ‘Level’
level.cpp:20: error: initializing argument 1 of ‘void SetLevel(Level)’

If I do:

SetLevel(0); // Line 18
SetLevel(-1);
SetLevel(2);

Bojan Komazec said...

I wrote at the end of the article that integers must be explicitly casted to enum type which you didn't do and therefore you got compiler error. You can use static_cast operator to achieve that: SetLevel(static_cast<Level>(0)). C cast would do the job as well (but avoid C casts in C++ code): SetLevel((Level)0). Of course, the best way is to use argument of the enum type: SetLevel(ZERO). Cheers