You can do it, you just need to replace constexpr
with const
in the header:
// declaration, possibly in header
extern const int i;
// source file
constexpr int i = 0;
The basic idea behind constexpr
for objects is:
- make it
const
- initialize it with a constant expression
The former part can be done by using const
in the header, latter part is only relevant to the initialization in the source file.
Is this really allowed?!
Yes. Let's look at the relevant sections in the standard:
Two declarations of entities declare the same entity if, [...], they correspond, have the same target scope that is not a function or template parameter scope, and either
- they appear in the same translation unit, or [...]
- they both declare names with external linkage.
- [basic.link] §8
In plain terms, both i
have the same name so they correspond, and they both have external linkage due to extern
.
For any two declarations of an entity E
:
- If one declares
E
to be a variable or function, the other shall declare E
as one of the same type.
- [...]
- [basic.link] §11
That begs the question: are two variables the same type if one is constexpr
and the other is const
?
A constexpr
specifier used in an object declaration declares the object as const
. [...]
- [dcl.constexpr] §6
The answer is yes, it just makes our object const
, it doesn't change the type in any other way. The only remaining question is whether we are allowed to put constexpr
on one declaration but not on another:
If any declaration of a function or function template has a constexpr
or consteval
specifier, then all its declarations shall contain the same specifier.
- [dcl.constexpr] §1
No, there are only restrictions for functions, not for variables. It is allowed to make one declaration const
and the other constexpr
.
It is allowed, but isn't it totally useless?
Not entirely. One possible use-case is if you have a large constexpr
lookup table that you want to compute at compile time, but don't want to put this initialization into a header, in order to reduce compile times.
If it's really important to compute this table at compile time, but not so important to have a definition of its contents visible everywhere (for inlining and optimizations), extern constexpr
can help.
inline constexpr
would require an identical definition (and initialization) everywhere to not violate the one definition rule. As a result, we incur the cost in every translation unit that includes our header.
static constexpr
is even worse, because every translation unit has its own copy of this large lookup table.
extern constexpr
covers this use-case perfectly.
Note: not all compilers conform to the standard by default. Use /Zc:externConstexpr
when compiling with MSVC.