4.13 — Const variables and symbolic constants

In programming, a constant is a value that may not be changed. C++ supports several types of constants: const variables (which we’ll cover in this lesson and 4.14 -- Compile-time constants, constant expressions, and constexpr), and literals (which we’ll cover shortly, in lesson 4.15 -- Literals).

Const variables

So far, all of the variables we’ve seen have been non-constant -- that is, their values can be changed at any time (typically through assignment of a new value). For example:

int main()
{
    int x { 4 }; // x is a non-constant variable
    x = 5; // change value of x to 5 using assignment operator

    return 0;
}

However, there are many cases where it is useful to define variables with values that can not be changed. For example, consider the gravity of Earth (near the surface): 9.8 meters/second2. This isn’t likely to change any time soon (and if it does, you’ve likely got bigger problems than learning C++). Defining this value as a constant helps ensure that this value isn’t accidentally changed. Constants also have other benefits that we’ll explore momentarily.

A variable whose value can not be changed is called a constant variable.

The const keyword

To make a variable a constant, place the const keyword in the variable’s declaration either before or after the variable type, like so:

const double gravity { 9.8 };  // preferred use of const before type
int const sidesInSquare { 4 }; // "east const" style, okay but not preferred

Although C++ will accept const either before or after the type, it’s much more common to use const before the type because it better follows standard English language convention where modifiers come before the object being modified (e.g. a “a green ball”, not a “a ball green”).

As an aside…

Due to the way that the compiler parses more complex declarations, some developers prefer placing the const after the type (because it is slightly more consistent). This style is called “east const”. While this style has some advocates (and some reasonable points), it has not caught on significantly.

Best practice

Place const before the type (because it is more conventional to do so).

Const variables must be initialized

Const variables must be initialized when you define them, and then that value can not be changed via assignment:

int main()
{
    const double gravity; // error: const variables must be initialized
    gravity = 9.9;        // error: const variables can not be changed

    return 0;
}

Note that const variables can be initialized from other variables (including non-const ones):

#include <iostream>

int main()
{ 
    std::cout << "Enter your age: ";
    int age{};
    std::cin >> age;

    const int constAge { age }; // initialize const variable using non-const value

    age = 5;      // ok: age is non-const, so we can change its value
    constAge = 6; // error: constAge is const, so we cannot change its value

    return 0;
}

In the above example, we initialize const variable constAge with non-const variable age. Because age is still non-const, we can change its value. However, because constAge is const, we cannot change the value it has after initialization.

Naming your const variables

There are a number of different naming conventions that are used for const variables.

Programmers who have transitioned from C often prefer underscored, upper-case names for const variables (e.g. EARTH_GRAVITY). More common in C++ is to use intercapped names with a ‘k’ prefix (e.g. kEarthGravity).

However, because const variables act like normal variables (except they can not be assigned to), there is no reason that they need a special naming convention. For this reason, we prefer using the same naming convention that we use for non-const variables (e.g. earthGravity).

Const function parameters

Function parameters can be made constants via the const keyword:

#include <iostream>

void printInt(const int x)
{
    std::cout << x << '\n';
}

int main()
{
    printInt(5); // 5 will be used as the initializer for x
    printInt(6); // 6 will be used as the initializer for x

    return 0;
}

Note that we did not provide an explicit initializer for our const parameter x -- the value of the argument in the function call will be used as the initializer for x.

Making a function parameter constant enlists the compiler’s help to ensure that the parameter’s value is not changed inside the function. However, when arguments are passed by value, we generally don’t care if the function changes the value of the parameter (since it’s just a copy that will be destroyed at the end of the function anyway). Making a parameter constant also adds clutter to the code. For these reasons, we usually don’t make parameters passed by value const.

Best practice

Don’t use const when passing by value.

Later in this tutorial series, we’ll talk about two other ways to pass arguments to functions: pass by reference, and pass by address. When using either of these methods, proper use of const is important.

Const return values

A function’s return value may also be made const:

#include <iostream>

const int getValue()
{
    return 5;
}

int main()
{
    std::cout << getValue() << '\n';

    return 0;
}

For fundamental types, the const qualifier on a return type is simply ignored (your compiler may generate a warning).

For other types (which we’ll cover later), there is typically little point in returning const objects by value, because they are temporary copies that will be destroyed anyway. Returning a const value can also impede certain kinds of compiler optimizations (involving move semantics), which can result in lower performance.

Best practice

Don’t use const when returning by value.

What is a symbolic constant?

A symbolic constant is a name that is given to a constant value. Constant variables are one type of symbolic constant, as a variable has a name (its identifier) and a constant value.

#include <iostream>

int main()
{
    const int maxStudentsPerClass { 30 }; // this is a symbolic constant
    std::cout << "The class has " << maxStudentsPerClass << " students.\n";

    return 0;
}

For symbolic constants, prefer constant variables over object-like macros

In lesson 2.10 -- Introduction to the preprocessor, we discussed that the preprocessor supports object-like macros with substitution text. These take the form:

#define identifier substitution_text

Whenever the preprocessor processes this directive, any further occurrence of identifier is replaced by substitution_text. The identifier is traditionally typed in all capital letters, using underscores to represent spaces.

For example:

#include <iostream>
#define MAX_STUDENTS_PER_CLASS 30

int main()
{
    std::cout << "The class has " << MAX_STUDENTS_PER_CLASS << " students.\n";

    return 0;
}

When compiling this program, the preprocessor will replace MAX_STUDENTS_PER_CLASS with the literal value 30, which the compiler will then compile into your executable.

Because object-like macros have a name, and the substitution text is a constant value, object-like macros with substitution text are also symbolic constants.

So why not use #define to make symbolic constants? There are (at least) three major problems.

First, because macros are resolved by the preprocessor, all occurrences of the macro are replaced with the defined value just prior to compilation. If you are debugging your code, you won’t see the actual value (e.g. 30) -- you’ll only see the name of the symbolic constant (e.g. MAX_STUDENTS_PER_CLASS). And because these #defined values aren’t variables, you can’t add a watch in the debugger to see their values. If you want to know what value MAX_STUDENTS_PER_CLASS resolves to, you’ll have to find the definition of MAX_STUDENTS_PER_CLASS (which could be in a different file). This can make your programs harder to debug.

Second, macros can have naming conflicts with normal code. For example:

#include "someheader.h"
#include <iostream>

int main()
{
    int beta { 5 };
    std::cout << beta << '\n';

    return 0;
}

If someheader.h happened to #define a macro named beta, this simple program would break, as the preprocessor would replace the int variable beta’s name with the macro’s substitution text. This is normally avoided by using all caps for macro names, but it can still happen.

Thirdly, macros don’t follow normal scoping rules, which means in rare cases a macro defined in one part of a program can conflict with code written in another part of the program that it wasn’t supposed to interact with.

Best practice

Prefer constant variables over object-like macros with substitution text.

Using constant variables throughout a multi-file program

In many applications, a given symbolic constant needs to be used throughout your code (not just in one location). These can include physics or mathematical constants that don’t change (e.g. pi or Avogadro’s number), or application-specific “tuning” values (e.g. friction or gravity coefficients). Instead of redefining these every time they are needed, it’s better to declare them once in a central location and use them wherever needed. That way, if you ever need to change them, you only need to change them in one place.

There are multiple ways to facilitate this within C++ -- we cover this topic in full detail in lesson 7.9 -- Sharing global constants across multiple files (using inline variables).

guest
Your email address will not be displayed
Find a mistake? Leave a comment above!
Correction-related comments will be deleted after processing to help reduce clutter. Thanks for helping to make the site better for everyone!
Avatars from https://gravatar.com/ are connected to your provided email address.
Notify me about replies:  
417 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments