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 (or const
in pure C). 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:
- It is an arbitrary value that has some explicit meaning (e.g., given us by a hardware specification), but such a meaning is not obvious so we need to name it for better readability. This becomes even more important if you have the same value used in completely different contexts (e.g.,
13
is the day number when Fridays get unlucky as well as the pin index of the first LED on Arduino fun-shield). - The same value is used in the code multiple times and there is a chance (albeit remote) you will need to change that value at some point (e.g., a timing parameter that remains constant at runtime, but the customer might request changing it in the future).
- The value is used only once (or very few times), but it is likely to change (possibly with other values together). For instance, Arduino pin indices of LEDs might change when the code is ported to a different shield. In other words, using constants in a header or at the beginning of your file can make it more convenient to tune/change these values at compile time.
There are also situations when using a constant would be an overkill or even a bad idea:
- When the value is obvious and has no special meaning (e.g., using constants for values like
0
or1
if they are used in basic arithmetic operations). - Values that are part of mathematical equations and that are fixed and will never change. A good decision rule, in this case, could be, whether you can devise a good name for the constant. If you cannot, do not turn these literals into constants.
Technical notes
- It is usually a good idea to have some naming convention for constants. In our course, we will use uppercase letters with underscores separating words (e.g.,
LED_PIN
). This is not a mandatory requirement, but it is a strong suggestion as it will make the code more readable. Variables (global, local, and class members) should be named in lower_case, CamelCase, or pascalCase (pick one and be consistent). - In C++, the
constexpr
keyword is preferred overconst
since it allows the compiler to evaluate the constant at compile time. Again, this is merely a strong suggestion which will help you get the right coding habits. - Some constants may depend on others. E.g. number of LEDs depends on the array where LED pins are listed. In such cases, one constant value should be computed from the other, like
constexpr int LEDS_COUNT = sizeof(LED_PINS) / sizeof(LED_PINS[0]);
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 |
|
|
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 |
|
|
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 |
|
|