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.
- 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.
- The compiler need not evaluate constexpr code, even if it is possible.
- In this sense,
constexpris a recommendation to the compiler. - If the compiler does not evaluate the constexpr code then it will be executed at run-time.
- In this sense,
- 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
constexprfunction from within astatic_assertwill force it to be evaluated at compile-time at that particular call site.
- For example, calling a
Why?
Using constexpr is useful in several ways:
- Increases the applicability of the code by allowing it to be used in compile-time contexts.
- Speed-up run-time code because the value can be computed once when you compile
- Allow testing at compile-time using
static_assert
Why Not?
- Not everything that can be done in C++ can be done in
constexprcode- The list of allowable items and constructs has expanded with each new standard
- It can increase compilation time
- The compiler is now evaluating code that otherwise would be evaluated when the program runs
- 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 theconstexpr, their code will break.
- If users rely on your code being
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
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
- We will now run the code in the debugger and interactively set it up:
gdb cexpr_demo(Load the executable into the debugger)set disassembly-flavor intel(Use intel assembly syntax)layout split(Interactively see C++ code and the Dissassembly)b main(Set a breakpoint on themain()function- You should see
b+next to the first line in themain()function
- You should see
r(Run the program, you can say [n] todebuginfodif asked
- The program runs until the breakpoint, with the current
C++andasmlines highlighted p const_volto see the value ofconst_volandp volto see the value ofvol.- The code to initialize these variables has not run, so right now their values can be anything and even differ between runs
- 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
0x1770into the address pointed to byrbp-0x8.- The hexadecimal number
0x1770is6000in decimal, which is the result ofconst_rect_volume(10, 20, 30)
- The hexadecimal number
- We can verify that
rbp-0x8 =&const_vol= by printing both:p $rbp-0x8andp &const_vol - The value
0x1770is hard-coded in the assembly, which means that it was computed by the compiler!
- This instruction corresponds to
- Use the command
sito step into the next assembly instruction- Each
sicommand advances by one assembly instruction - When all the instructions corresponding to a line of
C++code are executed, the activeC++code line advances
- Each
- Notice that a single
sicommand advances theC++line, indicating thatauto const_vol = const_rect_volume(10, 20, 30)was reduced to a single assembly instruction by the compiler! - The next assignment is
auto vol = rect_volume(10, 20, 30), which is notconstexpr- The number of times you must run
sito advance theC++line is the number of instructions it takes - Run
siuntil you advance to the nextC++line inmain()and count the instructions. - Verify the result with
p vol
- The number of times you must run
- Use
cto continue running the program and see it's output - Use
qto quitgdb
Generating the Assembly
- You can also view the generated assembly language code, annotated by the compiler:
- Use
g++ -S -masm=intel -fverbose-asm -g -std=c++23 constexpr_demo.cppto generate assembly code inconstexpr_demo.s-S: Generate assembly code-masm=intel: Use intel syntax-fverbose-asm: Interleave C++ code with the assembly-gInclude debugging information
- Open
constexpr_demo.s- This is a complete assembly listing of the code, along with the corresponding C++ lines
- Do a text search for
auto const_vol- Notice the assembly corresponding to this assignment is a single instruction
(the
.locis an assembler directive, not code).
- Notice the assembly corresponding to this assignment is a single instruction
(the
- 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)
- To find the code for
_Z11rect_volumeiiisearch for_Z11rect_volumeiii:- Notice that the function consists of a lot of assembly instructions
- Search the code for
return length * width * height- Notice that all references refer to the
returnstatement inrect_volume - There is no corresponding code for
const_rect_volumebecause the compiler is evaluating it at compile-time and therefore it does not need to even emit runtime code.
- Notice that all references refer to the
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.
- The
C++code andasmcode are shown side by side. - The
C++code andasmcode are color coded to show the correspondence betweenC++andasminstructions - Selecting a line and pressing
Ctrl F10(or right clicking) allows you to jump to the appropriate assembly code. - The original code is available. Take a look and note the following:
- The
const_volassignment corresponds to one line of assembly - The
volassignment consists of many lines, including a function call - The
const_rect_volumefunction 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.
- The
- Let's modify the code so the call to
const_rect_volumetakes a parameter that is not aconstexpr- For example, instead of specifying all the parameters as integer literals, store one of them in a variable first.
- Notice that
const_rect_volumenow does have corresponding assembly code and it is available at runtime (and therefore included in the assembly output)
- 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_volumeincluded in the code (but notconst_rect_volume)
- If optimization can do what
constexprcan then whyconstexpr?- The optimizer can not always turn all functions with compile-time arguments into a single result:
- We got lucky because the function is simpler
- 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
- Non
constexprfunctions cannot be used inconstant expressioncontexts, regardless of the optimization level
- The optimizer can not always turn all functions with compile-time arguments into a single result: