CPP‐Constants - rFronteddu/general_wiki GitHub Wiki

A constant is a value that may not be changed during the program’s execution.

C++ supports two different kinds of constants:

  • Named constants/symbolic constants are constant values that are associated with an identifier.
  • Literal constants are constant values that are not associated with an identifier.

Named Constants

There are three ways to define a named constant in C++:

  • Constant variables
  • Object-like macros with substitution text
  • Enumerated constants

To declare a constant variable, we place the const keyword (called a “const qualifier”) adjacent to the object’s type (because it is more conventional to do so):

const double gravity { 9.8 };

The type of an object includes the const qualifier, so when we define const double gravity { 9.8 }; the type of gravity is const double. Const variables must be initialized when you define them, and then that value can not be changed via assignment.

More common in C++ is to use intercapped names with a ‘k’ prefix (e.g. kEarthGravity).

Function parameters can be made constants via the const keyword

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

Making a function parameter constant enlists the compiler’s help to ensure that the parameter’s value is not changed inside the function. However, in modern C++ we don’t make value parameters const because 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). The const keyword also adds a small amount of unnecessary clutter to the function prototype. Don’t use const for value parameters. When using pass by reference, and pass by address, proper use of const is important.

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

const int getValue()
{
    return 5;
}

For fundamental types, the const qualifier on a return type is simply ignored. 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. Don’t use const when returning by value.

Make variables constant whenever possible. Exception cases include by-value function parameters and by-value return types, which should generally not be made constant.

Object-like macros with substitution text

Prefer constant variables to preprocessor macros.

  • The biggest issue is that macros don’t follow normal C++ scoping rules. Once a macro is #defined, all subsequent occurrences of the macro’s name in the current file will be replaced. If that name is used elsewhere, you’ll get macro substitution where you didn’t want it. This will most likely lead to strange compilation errors.
  • Second, it is often harder to debug code using macros. Although your source code will have the macro’s name, the compiler and debugger never see the macro because it has already been replaced before they run. Many debuggers are unable to inspect a macro’s value, and often have limited capabilities when working with macros.
  • Third, macro substitution behaves differently than everything else in C++. Inadvertent mistakes can be easily made as a result.

Using constant variables throughout a multi-file program

Nomenclature: type qualifiers

A type qualifier (sometimes called a qualifier for short) is a keyword that is applied to a type that modifies how that type behaves. The const used to declare a constant variable is called a const type qualifier (or const qualifier for short). As of C++23, C++ only has two type qualifiers: const and volatile.

The volatile qualifier is used to tell the compiler that an object may have its value changed at any time. This rarely-used qualifier disables certain types of optimizations.

Literals

Literals (or literal constants) are values that are inserted directly into the code. The type of a literal is deduced from the literal’s value. If the default type of a literal is not as desired, you can change the type of a literal by adding a suffix (u/U, I/L, f, uz, s, sv). Prefer literal suffix L (upper case) over l (lower case).

return 5;                     // 5 is an integer litera

String literals

String literals are placed between double quotes to identify them as strings (as opposed to char literals, which are placed between single quotes). For historical reasons, strings are not a fundamental type in C++. Rather, they have a strange, complicated type that is hard to work with. Such strings are often called C strings or C-style strings, as they are inherited from the C-language.

There are two non-obvious things worth knowing about C-style string literals.

  • All C-style string literals have an implicit null terminator. Consider a string such as "hello". While this C-style string appears to only have five characters, it actually has six: 'h', 'e', 'l‘, 'l', 'o', and '\0' (a character with ASCII code 0). This trailing ‘\0’ character is a special character called a null terminator, and it is used to indicate the end of the string. A string that ends with a null terminator is called a null-terminated string.
  • Unlike most other literals (which are values, not objects), C-style string literals are const objects that are created at the start of the program and are guaranteed to exist for the entirety of the program. This fact will become important in a few lessons, when we discuss std::string_view.

Unlike C-style string literals, std::string and std::string_view literals create temporary objects. These temporary objects must be used immediately, as they are destroyed at the end of the full expression in which they are created.

Magic numbers

A magic number is a literal (usually a number) that either has an unclear meaning or may need to be changed later.

const int maxStudentsPerSchool{ numClassrooms * 30 };
setMax(30);

What do the literals 30 mean in these contexts? In the former, you can probably guess that it’s the number of students per class, but it’s not immediately obvious. In the latter, who knows. We’d have to go look at the function to know what it does.

Using magic numbers is generally considered bad practice because, in addition to not providing context as to what they are being used for, they pose problems if the value needs to change.

Fortunately, both the lack of context and the issues around updating can be easily addressed by using symbolic constants

const int maxStudentsPerClass { 30 };
const int totalStudents{ numClassrooms * maxStudentsPerClass }; // now obvious what this 30 means

const int maxNameLength{ 30 };
setMax(maxNameLength); // now obvious this 30 is used in a different context

Note that magic numbers aren’t always numbers -- they can also be text (e.g. names) or other types.

Constant Expressions

By default, expressions evaluate at runtime. In a few other cases, the C++ language requires an expression that can be evaluated at compile-time. For example, constexpr variables require an initializer that can be evaluated at compile-time.

A few common cases where a compile-time evaluatable expression is required:

  • The initializer of a constexpr variable.
  • A non-type template argument.
  • The defined length of a std::array or a C-style array.

The C++ language provides ways for us to be explicit about what parts of code we want to execute at compile-time. The use of language features that result in compile-time evaluation is called compile-time programming.

Compile-time evaluation allows us to write programs that are both more performant and of higher quality (more secure and less buggy)! So while compile-time evaluation does add some additional complexity to the language, the benefits can be substantial.

The following C++ features are the most foundational to compile-time programming:

  • Constexpr variables
  • Constexpr functions
  • Templates
  • static_assert

All of these features have one thing in common: they make use of constant expressions.

Constant expression