NSWI170 Computer Systems

Guidelines to write a better C/C++ code

previous curse | all curses | next curse

Unforgivable curse #2: Constants

Constants allow us to create named immutable values which can be used in the code expressions like literals or variables. They are declared similarly to variables, but the declaration is prefixed by constexpr keyword (actually, we can also use const keyword, but there is a difference and we are not getting into the details here). In this text, we will try to explain, why constants can be beneficial, where to use them, and where not to use them.

Motivation

Utilization of constants (constexpr) may lead to more understandable and in some cases even more portable code. There are two main reasons for that. First, it is better if an arbitrary value (like a pin index of Arduino) is labeled with some human-readable text, so everyone can understand it immediately. Second, the constant is used in many places, but you assume that it needs to be changed from time to time (i.e., at compile time, not at runtime). So when the change comes, you need to make it at exactly one place, not throughout the code.

Explanation

The actual rules for constants were almost described already in motivation, so let us spell them out in more detail. A literal value (e.g., a number) in the code should be replaced with a constant if one of the following indicators holds:

There are also situations when using a constant would be an overkill or even a bad idea:

Examples

The following code demonstrates a simple application that sets the state of the output LED based on the input button. Compare the bad code and the good code from the perspective of readability (especially when the good code lacks comments). Also note, that the constants could be placed in a header file that will be included. If we use multiple similar hardware devices (e.g., Arduino shields), we may have such a header for each shield (with the same constant names but different values), so porting your code to another shield would require only including a different header.

πŸ’© Bad code πŸ‘ Good code
int main() {
    initialize_pin(42, 0); // LED for writing
    initialize_pin(54, 1); // button for reading

    if (get_pin_state(54) == 0) { // button is down
        set_pin_state(42, 1); // turn LED on
    } else {
        set_pin_state(42, 0); // turn LED off
    }
}
// these constants may be in a separate header file
constexpr int led_pin = 42;
constexpr int button_pin = 54;
constexpr int LED_ON = 1;
constexpr int LED_OFF = 0;
constexpr int BTN_DOWN = 0;
constexpr int OUTPUT = 0;
constexpr int INPUT = 1;

int main() {
    initialize_pin(led_pin, OUTPUT);
    initialize_pin(button_pin, INPUT);

    set_pin_state(led_pin, get_pin_state(button_pin) == BTN_DOWN
        ? LED_ON : LED_OFF);
}

The integers are not the only values we can put in constants. Creating a constant array may be helpful to create lookup/translation tables or predefined sequences. In the following example, the core prints out characters intended to resemble a rotating cursor. The imperative approach (bad code) is quite bulky and tedious whilst the declarative approach (good code) is short and neat. Furthermore, altering the animation in the good code is just a matter of changing values in one constant array. Similarly, if we decide that the animation is loaded dynamically (e.g., from a file), the main code would not have to be changed at all.

Note: the print() function is not a standard function. We have used it as an abstraction to hide C++ streams which might scare off frightened beginners.

πŸ’© Bad code πŸ‘ Good code
int main() {
    int step = 0;
    while (true) {
        switch (step) {
            case 0:
                print('-');
                break;
            case 1:
                print('\\');
                break;
            case 2:
                print('|');
                break;
            case 3:
                print('/');
                break;
        }
        step = step + 1;
        if (step > 3) {
            step = 0;
        }
    }
}
constexpr char animation[] = { '-', '\\', '|', '/' };
constexpr int animation_steps =
    sizeof(animation) / sizeof(animation[0]);

int main() {
    int step = 0;
    while (true) {
        print(animation[step]);
        step = (step + 1) % animation_steps;
    }
}

Finally, let us present an example, where using constants will actually not help the situation. Using names like fahrenheit_to_celsius_diff is awkward at best and it may not be clear, whether the diff should be added or subtracted and whether to apply the diff first or the factor first. Furthermore, the mnemonic role (adding "labels" to pieces of code) is better handled by a function which kind of encapsulates the utilization of constants (where the actual values remain as code literals).

πŸ’© Bad code πŸ‘ Good code
constexpr float fahrenheit_to_celsius_diff = 32.0f;
constexpr float fahrenheit_to_celsius_factor = 5.0f / 9.0;
// ...
float celsius = (fahrenheit - fahrenheit_to_celsius_diff)
                    * fahrenheit_to_celsius_factor;
// a function handles this case better than individual constants
float fahrenheit_to_celsius(float f)
{
    return (f - 32.0f) * 5.0f / 9.0f;
}