inline and constexpr Functions
Two function specifiers you will encounter frequently in C++ codebases: inline and constexpr. Each one changes something important about how the function is compiled. inline controls linkage rules that matter when a function is defined in a header. constexpr tells the compiler that the function may be evaluated entirely at compile time, enabling its results to be used in contexts that require compile-time constants — array sizes, template arguments, and more.
inline — the ODR exception
The One Definition Rule (ODR) says that a function may be defined only once across all the translation units (source files) that make up a program. A declaration (prototype) can appear many times, but the body only once. This creates a problem for header files: if you write a function definition in a .h file and include that file in two .cpp files, the linker sees two identical definitions — an ODR violation and a linker error.
The inline keyword is the standard exception to the ODR. Marking a function inlinetells the linker: “you may encounter multiple identical definitions of this function across translation units — they are all the same, pick one and discard the rest.” This makes it safe to define the function in a header that is included everywhere.
// math_utils.h — safe to include from multiple .cpp files:
#pragma once
inline double square(double x) {
return x * x;
}
// Without inline, including this header from two .cpp files would cause a
// linker error: "multiple definition of square(double)"The name “inline” is historical: it originally hinted to the compiler that it should substitute the function body at the call site rather than generating a real function call — an optimisation called inlining. Modern compilers ignore the hint entirely and inline functions based on their own heuristics. A [[nodiscard]] hint is more likely to change compiler behaviour today than inline is.
| Context | Implicitly inline? |
|---|---|
| Member function defined inside a class body | Yes |
| constexpr function | Yes |
| Free function defined in a header with inline keyword | Yes (explicitly) |
| Free function defined in a .cpp file | No |
| Function template instantiation | Yes |
In practice, you will mostly encounter inline in header-only libraries, where every function must be defined in the header. If you are writing a normal project where functions are defined in .cpp files, you rarely need inline explicitly.
What is a constant expression?
Certain positions in C++ code require a value that the compiler can compute before the program runs. These are called constant expressions. They appear as: array sizes, switch case values, non-type template arguments (N in std::array<int, N>), and initializers for constexpr variables. What all these positions have in common is that the compiler must evaluate them at compile time — they are manifestly constant-evaluated.
// All of these require compile-time constants:
std::array<int, 10> arr; // 10 — literal, always OK
const std::size_t N {5};
std::array<int, N> arr2; // OK: integer const with literal initializer
std::size_t variable {5}; // non-const!
// std::array<int, variable> arr3; // ERROR: not a constant expression
const double pi {3.14159};
// std::array<int, static_cast<int>(pi)> arr4; // ERROR: const double is not a
// constant expression (surprising!)
// To fix: make it constexpr (see below)The surprise here is that constdoes not mean “known at compile time.” constmeans “not modifiable at runtime” — but a const variable could in principle be initialised from a runtime value (like user input). Only constexpr makes this a compile-time guarantee.
constexpr variables
Adding constexpr to a variable definition makes two promises: the value is computed at compile time, and the variable is const (implied, so you can omit const). The initializer must itself be a constant expression — any non-constexpr value on the right-hand side is a compile error.
constexpr int MAX_SIZE {1024}; // OK: integer literal
constexpr double PI {3.14159265}; // OK: floating-point literal
constexpr double TWO_PI {2.0 * PI}; // OK: constexpr * constexpr
const double runtime_pi {3.14}; // const but NOT constexpr
// constexpr double bad {runtime_pi}; // ERROR: runtime_pi may be runtime
// Use constexpr to unlock compile-time contexts:
std::array<double, MAX_SIZE> buffer; // OK
// std::array<double, static_cast<int>(PI)>; // now OK too (PI is constexpr)Mark every compile-time constant as constexpr. This is more than a style preference: it is a public promise to the code's users that the value will forever be evaluable at compile time. Removing constexpr from a constant is a breaking API change.
constexpr functions
A function without constexpr cannot be called in a constant expression — even if its body is trivially computable. Adding constexpr to a function definition signals that the function may be evaluated at compile time when its arguments are themselves constant expressions. The same function continues to work at runtime when called with runtime arguments.
// Without constexpr: cannot be called in a constant expression
std::size_t power(std::size_t x, unsigned n)
{
return n == 0 ? 1 : x * power(x, n - 1);
}
// std::array<int, power(2, 3)> arr; // ERROR: power() is not constexpr
// With constexpr: usable both at compile time and at runtime
constexpr std::size_t power(std::size_t x, unsigned n)
{
return n == 0 ? 1 : x * power(x, n - 1);
}
std::array<int, power(2, 3)> arr; // OK: evaluated to std::array<int, 8>
int user_input;
std::cin >> user_input;
auto result = power(user_input, 3); // Also OK: evaluated at runtimeA constexpr function is automatically inline — the compiler needs to see its body to evaluate it at compile time, so it is safe to put in a header file. During constant evaluation, only other constexpr functions may be called. No operating-system operations (file I/O, console output, memory allocation from the heap) are permitted.
// A practical constexpr function: compile-time string length
constexpr std::size_t str_len(const char* s) {
std::size_t n {0};
while (s[n] != '\0') ++n;
return n;
}
constexpr std::size_t len = str_len("hello"); // 5 — computed at compile time
static_assert(len == 5); // verified at compile time
// Compile-time power-of-two check:
constexpr bool is_power_of_two(unsigned n) {
return n > 0 && (n & (n - 1)) == 0;
}
static_assert(is_power_of_two(64));
static_assert(!is_power_of_two(63));static_assert is a compile-time assertion: if the condition is false, the program fails to compile with a clear error message. Pairing static_assert with constexpr functions is a powerful technique for catching logic errors at compile time rather than at runtime.
consteval — compile-time only (C++20)
A constexpr function runs at compile time when it can, but still compiles to regular runtime code when called with runtime arguments. consteval (C++20) removes that flexibility: a consteval function is an immediate function that may only be evaluated at compile time. The compiler generates no binary code for it — if you call it with a runtime value, the program fails to compile.
consteval int square_ct(int x) { return x * x; }
constexpr int a {square_ct(5)}; // OK: 5 is a constant expression
// int y = 3;
// int b = square_ct(y); // ERROR: y is not a constant expression;
// consteval functions cannot be called at runtime
// Compare with constexpr:
constexpr int square(int x) { return x * x; }
int y = 3;
int c = square(y); // OK: square() also works at runtimeUse consteval when you are writing a function that is only meaningful at compile time — for example, generating lookup tables, processing format strings, or validating configuration. The C++ Standard Library itself uses consteval internally to validate std::format and std::print format strings at compile time, which is why a typo in a format specifier is a compile error, not a runtime exception.
Quick comparison
| Specifier | Implies inline? | Compile-time call? | Runtime call? | Main use |
|---|---|---|---|---|
| (plain function) | No | Only if compiler chooses | Yes | Normal runtime logic |
| inline | Yes (it IS inline) | Only if compiler chooses | Yes | Define functions in headers without ODR violations |
| constexpr | Yes | Yes — when args are constexpr | Yes | Compile-time AND runtime computation |
| consteval | Yes | Yes — always | No | Compile-time-only computation |
Key rules to remember
const ≠ constexpr for non-integers
A const variable might be initialised from a runtime value — the compiler cannot use it in constant-evaluated contexts. Only constexpr guarantees compile-time availability.
constexpr functions are both compile-time and runtime
Adding constexpr does not remove runtime functionality. The same function works with runtime arguments too. Use consteval if you want compile-time only.
constexpr functions may only call other constexpr functions during constant evaluation
No OS calls, no I/O, no runtime-only library functions. At runtime (when called with non-constexpr args), the restriction is lifted.
constexpr implies inline
You can safely put constexpr functions in header files without ODR problems. No explicit inline needed.
inline's modern purpose is ODR, not inlining
Compilers inline functions regardless of the keyword. Use inline when you define a function in a header file that will be included by multiple translation units.