UP | HOME

Exploring Constexpr

Constexpr In C++

What?

In C++, the constexpr specifier tells the compiler that the affected declaration (e.g., function) may be evaluated at compile-time or at run-time.

  1. Compile-time evaluation is only possible if all inputs are constant expressions.
    • Essentially, the inputs must are known to the compiler and are constant.
    • The definition of the phrase constant expression and the keyword constexpr are related but distinct.
  2. The compiler need not evaluate constexpr code, even if it is possible.
    • In this sense, constexpr is a recommendation to the compiler.
    • If the compiler does not evaluate the constexpr code then it will be executed at run-time.
  3. If the constexpr code is used in the context of a constant expression then the code must be evaluated at compile-time
    • For example, calling a constexpr function from within a static_assert will force it to be evaluated at compile-time at that particular call site.

Why?

Using constexpr is useful in several ways:

  1. Increases the applicability of the code by allowing it to be used in compile-time contexts.
  2. Speed-up run-time code because the value can be computed once when you compile
  3. Allow testing at compile-time using static_assert

Why Not?

  1. Not everything that can be done in C++ can be done in constexpr code
    • The list of allowable items and constructs has expanded with each new standard
  2. It can increase compilation time
    • The compiler is now evaluating code that otherwise would be evaluated when the program runs
  3. It is a promise to future users:
    • If users rely on your code being constexpr (e.g., by using it in constant expression contexts) and you ever need to remove the constexpr, their code will break.

Activity

The Code

Here is some code to compute the volume of a rectangular prism:

// This code uses iostreams instead of print because it results in
// generated asm code that is easier to follow.
#include<iostream>
using std::cout;
using std::endl;

constexpr int const_rect_volume(int length,
                              int width,
                              int height)
{
    return length * width * height;
}

int rect_volume(int length, int width, int height)
{
    return length * width * height;
}

int main()
{
    auto const_vol = const_rect_volume(10, 20, 30);
    auto vol = rect_volume(10, 20, 30);
    cout << "const_vol: " << const_vol
         << " vol: " << vol << endl;
    return 0;
}

Live Inspection

  1. Compile the code, run it, and verify that it works:

    g++ -std=c++23 -g -o cexpr_demo constexpr_demo.cpp
    ./cexpr_demo
    # const_vol: 6000 vol: 6000
    
  2. We will now run the code in the debugger and interactively set it up:
    1. gdb cexpr_demo (Load the executable into the debugger)
    2. set disassembly-flavor intel (Use intel assembly syntax)
    3. layout split (Interactively see C++ code and the Dissassembly)
    4. b main (Set a breakpoint on the main() function
      • You should see b+ next to the first line in the main() function
    5. r (Run the program, you can say [n] to debuginfod if asked
  3. The program runs until the breakpoint, with the current C++ and asm lines highlighted
  4. p const_vol to see the value of const_vol and p vol to see the value of vol.
  5. The code to initialize these variables has not run, so right now their values can be anything and even differ between runs
  6. The current assembly instruction is mov DWORD PTR [rbp-0x8], 0x1770
    • This instruction corresponds to auto const_vol = const_rect_volume(10, 20, 30)
    • This instruction moves the second operand 0x1770 into the address pointed to by rbp-0x8.
      • The hexadecimal number 0x1770 is 6000 in decimal, which is the result of const_rect_volume(10, 20, 30)
    • We can verify that rbp-0x8 = &const_vol= by printing both: p $rbp-0x8 and p &const_vol
    • The value 0x1770 is hard-coded in the assembly, which means that it was computed by the compiler!
  7. Use the command si to step into the next assembly instruction
    • Each si command advances by one assembly instruction
    • When all the instructions corresponding to a line of C++ code are executed, the active C++ code line advances
  8. Notice that a single si command advances the C++ line, indicating that auto const_vol = const_rect_volume(10, 20, 30) was reduced to a single assembly instruction by the compiler!
  9. The next assignment is auto vol = rect_volume(10, 20, 30), which is not constexpr
    • The number of times you must run si to advance the C++ line is the number of instructions it takes
    • Run si until you advance to the next C++ line in main() and count the instructions.
    • Verify the result with p vol
  10. Use c to continue running the program and see it's output
  11. Use q to quit gdb

Generating the Assembly

  1. You can also view the generated assembly language code, annotated by the compiler:
  2. Use g++ -S -masm=intel -fverbose-asm -g -std=c++23 constexpr_demo.cpp to generate assembly code in constexpr_demo.s
    • -S: Generate assembly code
    • -masm=intel: Use intel syntax
    • -fverbose-asm: Interleave C++ code with the assembly
    • -g Include debugging information
  3. Open constexpr_demo.s
    • This is a complete assembly listing of the code, along with the corresponding C++ lines
  4. Do a text search for auto const_vol
    • Notice the assembly corresponding to this assignment is a single instruction (the .loc is an assembler directive, not code).
  5. Immediately below is the assembly code for auto vol = rect_volume(10, 20, 30)
    • Notice how there are many instructions corresponding to this line
    • Notice that one of the instructions is a function call (call _Z11rect_volumeiii)
  6. To find the code for _Z11rect_volumeiii search for _Z11rect_volumeiii:
    • Notice that the function consists of a lot of assembly instructions
  7. Search the code for return length * width * height
    • Notice that all references refer to the return statement in rect_volume
    • There is no corresponding code for const_rect_volume because the compiler is evaluating it at compile-time and therefore it does not need to even emit runtime code.

Compiler Explorer

The Compiler Explorer is an online tool that lets you easily compile your code with many different compilers and for many different architectures in order to view the generated assembly.

  1. The C++ code and asm code are shown side by side.
  2. The C++ code and asm code are color coded to show the correspondence between C++ and asm instructions
  3. Selecting a line and pressing Ctrl F10 (or right clicking) allows you to jump to the appropriate assembly code.
  4. The original code is available. Take a look and note the following:
    • The const_vol assignment corresponds to one line of assembly
    • The vol assignment consists of many lines, including a function call
    • The const_rect_volume function is not highlighted in any color: this is because the code does not exist in the assembly because it was evaluated at compile time and not needed at run time.
  5. Let's modify the code so the call to const_rect_volume takes a parameter that is not a constexpr
    • For example, instead of specifying all the parameters as integer literals, store one of them in a variable first.
    • Notice that const_rect_volume now does have corresponding assembly code and it is available at runtime (and therefore included in the assembly output)
  6. Add -O2 to the compiler flags:
    • Notice that optimizations have resulted in the compiler doing all the calculations for both functions at compile time.
    • However, there is still an implementation of rect_volume included in the code (but not const_rect_volume)
  7. If optimization can do what constexpr can then why constexpr?
    1. The optimizer can not always turn all functions with compile-time arguments into a single result:
      • We got lucky because the function is simpler
    2. It is much easier to debug programs that do not have optimizations enabled, so it's useful to get the compile-time evaluation even without compiler optimizations
    3. Non constexpr functions cannot be used in constant expression contexts, regardless of the optimization level

Author: Matthew Elwin.