Search

D.2.9 — Const, constexpr, and symbolic constants

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. For example:

However, it’s sometimes useful to define variables with values that can not be changed. For example, consider the value of gravity on Earth: 9.8 meters/second^2. This isn’t likely to change any time soon. Defining this value as a constant helps ensure that this value isn’t accidentally changed.

To make a variable constant, simply put the const keyword either before or after the variable type, like so:

Although C++ will accept const either before or after the type, we recommend using it before the type because it better follows standard English language convention where modifiers come before the object being modified (e.g. a green ball, not a ball green).

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

Declaring a variable as const prevents us from inadvertently changing its value:

Defining a const variable without initializing it will also cause a compile error:

Note that const variables can be initialized from non-const values:

Const is used most often with function parameters:

Making a function parameter const does two things. First, it tells the person calling the function that the function will not change the value of myValue. Second, it ensures that the function doesn’t change the value of myValue.

With parameters passed by value, like the above, 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). For this reason, we usually don’t const parameters passed by value. But later on, we’ll talk about other kinds of function parameters (where changing the value of the parameter will change the value of the argument passed in). For these types of parameters, judicious use of const is important.

Compile time vs runtime

When you are in the process of compiling your program, that is called compile time. During compile time, the compiler ensures your code is syntactically correct, and converts your code into object files.

When you are in the process of running your application, that is called runtime. During runtime, your program executes line by line.

Constexpr

C++ actually has two different kinds of constants.

Runtime constants are those whose initialization values can only be resolved at runtime (when your program is running). Variables such as usersAge and myValue above are runtime constants, because the compiler can’t determine their values at compile time. usersAge relies on user input (which can only be given at runtime) and myValue depends on the value passed into the function (which is only known at runtime).

Compile-time constants are those whose initialization values can be resolved at compile-time (when your program is compiling). Variable gravity above is an example of a compile-time constant. Whenever gravity is used, the compiler can simply substitute the identifier gravity for the literal double 9.8.

In most cases, it doesn’t matter whether a constant value is runtime or compile-time. However, there are a few odd cases where C++ requires a compile-time constant instead of a run-time constant (such as when defining the length of a fixed-size array -- we’ll cover this later). Because a const value could be either runtime or compile-time, the compiler has to keep track of which kind of constant it is.

To help provide more specificity, C++11 introduced new keyword constexpr, which ensures that the constant must be a compile-time constant:

Rule: Any variable that should not change values after initialization and whose initializer is known at compile-time should be declared as constexpr.
Rule: Any variable that should not change values after initialization and whose initializer is not known at compile-time should be declared as const.

Note: Many of these tutorials were written before constexpr existed and have not been fully updated. In practical terms, this isn’t an issue, as constexpr is only required in specific situations.

Naming your const variables

Some programmers prefer to use all upper-case names for const variables. Others use normal variable names with a ‘k’ prefix. However, we will use normal variable naming conventions, which is more common. Const variables act exactly like normal variables in every case except that they can not be assigned to, so there’s no particular reason they need to be denoted as special.

Symbolic constants

In the previous lesson D.2.8 -- Literals, we discussed “magic numbers”, which are literals used in a program to represent a constant value. Since magic numbers are bad, what should you do instead? The answer is: use symbolic constants! A symbolic constant is a name given to a constant literal value. There are two ways to declare symbolic constants in C++. One of them is good, and one of them is not. We’ll show you both.

Bad: Using object-like macros with a substitution parameter as symbolic constants

We’re going to show you the less desirable way to define a symbolic constant first. This method was commonly used in a lot of older code, so you may still see it.

In lesson 2.10 -- Introduction to the preprocessor, you learned that object-like macros have two forms -- one that doesn’t take a substitution parameter (generally used for conditional compilation), and one that does have a substitution parameter. We’ll talk about the case with the substitution parameter here. That takes the form:

#define identifier substitution_text

Whenever the preprocessor encounters 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.

Consider the following snippet:

When you compile your code, the preprocessor replaces all instances of MAX_STUDENTS_PER_CLASS with the literal value 30, which is then compiled into your executable.

You’ll likely agree that this is much more intuitive than using a magic number for a couple of reasons. MAX_STUDENTS_PER_CLASS provides context for what the program is trying to do, even without a comment. Second, if the number of max students per classroom changes, we only need to change the value of MAX_STUDENTS_PER_CLASS in one place, and all instances of MAX_STUDENTS_PER_CLASS will be replaced by the new literal value at the next compilation.

Consider our second example, using #define symbolic constants:

In this case, it’s clear that MAX_STUDENTS_PER_CLASS and MAX_NAME_LENGTH are intended to be independent values, even though they happen to share the same value (30).

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

First, because macros are resolved by the preprocessor, which replaces the symbolic name with the defined value, #defined symbolic constants do not show up in the debugger (which shows you your actual code). So although the compiler would compile int max_students = numClassrooms * 30;, in the debugger you’d see int max_students = numClassrooms * MAX_STUDENTS_PER_CLASS;. You’d have to go find the definition of MAX_STUDENTS_PER_CLASS in order to know what the actual value was. This can make your programs harder to debug.

Second, #defined values always have file scope (which we’ll talk more about in the section on local and global variables). This means a value #defined in one piece of code may have a naming conflict with a value #defined within the same file later.

For example:

In the above code, we #define x in function a(), intending it to be used inside function a(). However, #define value x can actually be used anywhere beyond that point in the same file. So when function b() #defines its own x, we get a naming conflict. Even if function b() used a variable named x instead of a #define value named x, we would still have issues, as the preprocessor would try to replace variable name x with the value 5.

Rule: Avoid using #define to create symbolic constants

A better solution: Use const variables

A better way to create symbolic constants is through use of const (or better, constexpr) variables:

These will show up in the debugger, and follow all of the normal variable rules around scope.

Rule: use const variables to provide a name and context for your magic numbers.

Using symbolic constants throughout a 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++, but the following is probably easiest:

1) Create a header file to hold these constants
2) Inside this header file, declare a namespace (we’ll talk more about this in lesson 4.3b -- Namespaces)
3) Add all your constants inside the namespace (make sure they’re const)
4) #include the header file wherever you need it

e.g. constants.h:

Use the scope resolution operator (::) to access your constants in .cpp files:

If you have both physics constants and per-application tuning values, you may opt to use two sets of files -- one for the physics values that will never change, and one for your per-program tuning values that are specific to your program. That way you can reuse the physics values in any program.

Note: In lesson 4.2 -- Global variables and linkage, we show a more efficient way to do symbolic constants using global variables.

D.2.10 -- Chapter D.2 comprehensive quiz
Index
D.2.8 -- Literals

165 comments to D.2.9 — Const, constexpr, and symbolic constants

  • Gejsi

    Can I name the namespace header file the exact same word as the namespace identifier, or is it a bad practice and gives a compile error?
    IE:

    • * Line 6, 7, 8: Initialize your variables with brace initializers. You used direct initialization.

      If the name that describes the contents of the file and the one that describes the contents of the namespace are the same, then you should name the file and namespace the same or similar. There won't be any errors.

  • In the "#define myconstant 2" vs "const int myconstatant = 2" discussion, I do wonder about one thing. I don't expect to often have to deal with it, but I recently wrote a small tool in C and I had to change my entire code when I wanted to make it MS-DOS compatible due to things not being fully supported. If I would ever code a tool which I intend to make runnable in DOS (either on real MS-DOS or DOSBox), should that 'force' me to use the "#define" method or is const also present in C++ compilers that age?

    • MS-DOS is an OS, C++ is a language, they're unrelated to each other.
      The closest guess I have is that you're running a compiler in MS-DOS that doesn't know about most of C++'s revisions (It probably only knows C++98). If that's the case, you can use the clang++ status site the see the changes in each version of C++ ( https://clang.llvm.org/cxx_status.html ).
      const has always been there.
      If you're talking about something else, please let me know.

  • Okorie Emmanuella

    Hello Nascardriver!

    is 2 not a magic number in the above snippet?

    • Hi!

      No. The formula for the circumference is well known (At least in combination with the variable name "circumference"). Also, there's no good name you could give to 2 in this case.
      Magic numbers occur when you don't name numbers that you made up yourself, like the height of a tower, a maximum score, etc..

  • Louis Cloete

    @Alex, if I find that I repeat a block of code similar to this:

    Would it make sense to define it as an object-like macro, then #undef'ing the macro after the fourth time? Just to be clear, it can not be inside a loop, because I change variables x and y between each time this sequence is executed.

    • Don't use macros unless you have to.
      You give little context about your code, here's my suggestion.

      • Louis Cloete

        The context is that I am writing a PvP chess game. The function takes a board coordinate as input and figures out if a Knight threatens a given square. Here it is:

        It is a bit ugly. It also has a lot of unsigned ints, because of std::array's sizeType. The gist is that if the change in one direction is +/- 2, the change in the other direction must be +/- 1, and vice versa. You gave me an idea, anyway. I can post when I implemented it if you want to.

        • The size type of @std::array doesn't have to be an unsigned int. Use boardArray_t::size_type instead.
          @searchPos in @findPiece never changes. There was also a spelling error in a variable name that prevented compilation. Make sure to test your program often during development to prevent getting overrun by problems in the end.
          My untested solution (Write a solution yourself first, post that)

          SPOILER SPOILER SPOILER

          • Louis Cloete

            I decided against using boardArray_t::sizeType, because I have an array of Pieces too, which is technically another type, and I want to use the same loop counter to access both sometimes. std::array::sizeType is an alias for unsigned int, so I use it. I would prefer to use ints, but the compiler doesn't like it. Maybe I should just remove the -Werror flag and use ints.

            Thanks for pointing out that searchPos never changes. I missed that.

            The spelling error was because I wrote the code with Afrikaans variable names. I copied it into the comment editing box and did a manual search and replace with English names. @board was originally @bord. I did compile just prior to asking my original question and didn't change anything since then until I copied the code into the comment editing box.

            My code:
            Note: I didn't use classes, since I started this project before I knew how to use them. I decided to finish it without using my own classes, then refactor it into classes as applicable. I also didn't translate again. You should be able to follow the gist of the code after the previous translated version.

            • * Line 5, 35, 36: Initialize your variables with uniform initialization.
              * Line 33, 49: Limit your lines to 80 characters in length for better readability on small displays.

              > Maybe I should just remove the -Werror flag and use ints.
              No. Error and warnings mean that you're doing something wrong. Disabling them will lead to trouble sooner or later.

              > I wrote the code with Afrikaans
              Program in Dutch and dutch people will help you. Program in English and the world will help you.

              * @soekStuk would be more efficient to use if it returned the Square it found, as any caller will most likely want to access the piece.
              * @pos should be declared in line 49
              * @stuk should be declared in line 53
              * @soekStuk can return by value. This will make your code easier and less prone to memory leaks. This won't slow down your code. Compilers have to elide the copy.

              • Louis Cloete

                Hi, @nascardriver!

                I have been afc for a while. I think you misunderstood my intention with the -Werror flag. Of course you shouldn't disable warnings. It is just that the choice to use unsigned ints for std::array's size type generates a lot of sign conversion warnings. Some of them are legit, and should be dealt with, while some of them are no problem. I can see that my int loop counter will never go negative. I am just counting from 0 up to 7 in a lot of cases. To fix that, I have to either static_cast to unsigned int (ugly) or use unsigned ints for loop counters (not always possible in my program for other reasons). Disabling -Werror will still give the warnings, just not stop compilation on them. This will allow me to "fix" the legit ones without bloating my code fixing bogus errors.

                About coding in English, I didn't think I would share this code with anyone except for personal programmer friends speaking Afrikaans. I always code in English if I intend to share / ask a wider audience.

                About the last asterisks: I return a pointer because you can do this with a pointer:

                It is also a very handy way to encode the information if an actual piece was found in the search to return nullptr for a blank search. What should I do to make it clear to the caller that no legal Piece was found in the current search if I return by value?

                I agree that it is better to return the Square, so I made the @soekStuk return a Square*

                • > Werror
                  If you remove -Werror and ignore the warnings about signed/unsigned mismatches you'll miss other warnings that might not be negligible, because you get used to seeing warnings. You might not like @static_cast now, but after using it for a while you'll no longer care about it.

                  > @soekStuk
                  3 options I can think of right now:

  • Jules

    #include "constants.h"
    double circumference = 2 * radius * constants::pi;

    Instead can I use :
    double circumference = 2 * radius * constants.pi;

    what is the use of the "." operator? is it discussed later?

    And whenever "std::cout" or "std::cin" is used, are cin and cout functions being accessed from the standard library using the scope resolution operator? I am confused because cin and cout dont seem to be constants

  • HUE Saki

    PLEASE HELP ME!

    error: invalid operands of types 'int' and 'double()' to binary 'operator*'

    why above error occur when I try to compile below program in my CODE::BLOCK

    CONSTANT.H

    ifndef CONSTANTS_H
    #define CONSTANTS_H

    // define your own namespace to hold constants
    namespace constants
    {
        constexpr double pi(3.14159);
        constexpr double avogadro(6.0221413e23);
        constexpr double my_gravity(9.2); // m/s^2 -- gravity is light on this planet
        // ... other related constants
    }
    #endif

    MAIN.cpp

    #include <iostream>
    #include "CONSTANT.H"

    int main()
    {

    double circumference = 2.0 * 7.0 * constants::pi;
    return 0;}

    • hailinh0708

      1. You forget the "#" sign before ifndef.
      Aside from that, it compiles fine for me.
      Note: The compiler error(s) that appears do not relate to the error you encountered.

Leave a Comment

Put all code inside code tags: [code]your code here[/code]