Since link can rotten, copping content of https://quuxplusone.github.io/blog/2022/07/08/inline-constexpr here as an answer. I'm not an author.
Browser plugin "Copy selection as markdown" did nice job.
The C++11 and C++14 standard libraries defined many constexpr global variables like this:
constexpr piecewise_construct_t piecewise_construct = piecewise_construct_t();
constexpr allocator_arg_t allocator_arg = allocator_arg_t();
constexpr error_type error_ctype = /*unspecified*/;
constexpr defer_lock_t defer_lock{};
constexpr in_place_t in_place{};
constexpr nullopt_t nullopt(/*unspecified*/{});
In C++17, all of these constexpr variables were respecified as inline constexpr
variables. The inline
keyword here means the same thing as it does on an inline
function: “This entity might be defined in multiple TUs; all those definitions are identical; merge them into one definition at link time.” If you look at the generated code for one of these variables in C++14, you’ll see something like this (Godbolt):
// constexpr in_place_t in_place{};
.section .rodata
.type _ZStL8in_place, @object
.size _ZStL8in_place, 1
_ZStL8in_place:
.zero 1
Whereas in C++17, you’ll see this instead:
// inline constexpr in_place_t in_place{};
.weak _ZSt8in_place
.section .rodata._ZSt8in_place,"aG",@progbits,_ZSt8in_place,comdat
.type _ZSt8in_place, @gnu_unique_object
.size _ZSt8in_place, 1
_ZSt8in_place:
.zero 1
The critical word in the latter snippet is comdat
; it means “Hey linker! Instead of concatenating the text of all .rodata._ZSt8in_place
sections together, you should deduplicate them, so that only one such section is included in the final executable!” There’s another minor difference in the name-mangling of std::in_place
itself: as an inline constexpr
variable it gets mangled into _ZSt8in_place
, but as a non-inline
(and therefore static
) variable it gets mangled into _ZStL8in_place
with an L
. Clang’s name-mangling code has this to say about the L
:
GCC distinguishes between internal and external linkage symbols in its mangling, to support cases like this that were valid C++ prior to DR426:
void test() { extern void foo(); }
static void foo();
On the C++ Slack, Ed Catmur showed an example of how this difference can be observed. This is a contrived example, for sure, but it does concretely demonstrate the difference between mere constexpr
(internal linkage, one entity per TU) and inline constexpr
(external linkage, one entity for the entire program).
// f.hpp
INLINE constexpr int x = 3;
inline const void *f() { return &x; }
using FT = const void*();
FT *alpha();
// alpha.cpp
#include "f.hpp"
FT *alpha() { return f; }
// main.cpp
#include <cassert>
#include <cstdio>
#include "f.hpp"
int main() {
assert(alpha() == f); // OK
assert(alpha()() == f()); // Fail!
puts("Success!");
}
$ g++ -std=c++17 -O2 main.cpp alpha.cpp -DINLINE=inline ; ./a.out
Success!
$ g++ -std=c++17 -O2 alpha.cpp main.cpp -DINLINE=inline ; ./a.out
Success!
$ g++ -std=c++17 -O2 main.cpp alpha.cpp -DINLINE= ; ./a.out
Success!
$ g++ -std=c++17 -O2 alpha.cpp main.cpp -DINLINE= ; ./a.out
a.out: main.cpp:5: int main(): Assertion `alpha()() == f()' failed.
Aborted
The difference between the last two command lines is whether the linker sees alpha.o
or main.o
first, and therefore whether it chooses to keep the definition of inline const void *f()
from alpha.cpp
or main.cpp
. If it keeps the one from alpha.cpp
, then the result of the expression alpha()()
will be the address of x
-from-alpha.cpp
. But, thanks to the inliner, the assertion in main
will be comparing that address against the address of x
-from-main.cpp
. When x
is marked inline
, there’s only one entity x
in the whole program, so the two x
s are the same and the assertion succeeds. But when x
is a plain old constexpr
variable, there are two different (internal-linkage) x
s with two different addresses, and so the assertion fails.
You can reproduce this behavior with libstdc++, by swapping the variable x
for a C++14 standard library variable like std::piecewise_construct
. The assertion in main
will pass when compiled with -std=c++17
and fail when compiled with -std=c++14
. This is because libstdc++ makes these variables conditionally inline
depending on the language mode (source):
#ifndef _GLIBCXX17_INLINE
# if __cplusplus > 201402L
# define _GLIBCXX17_INLINE inline
# else
# define _GLIBCXX17_INLINE
# endif
#endif
_GLIBCXX17_INLINE constexpr
piecewise_construct_t piecewise_construct =
piecewise_construct_t();
LLVM/Clang’s libc++, on the other hand, doesn’t conditionalize their code based on the language mode (source):
/* inline */ constexpr
piecewise_construct_t piecewise_construct =
piecewise_construct_t();
I speculate that this was done in order to reduce the confusion that could result if a program was compiled partly as C++14 and partly as C++17. It’s bad enough that a program’s behavior can change (in this contrived scenario) depending on whether it’s compiled as C++14 or C++17; imagine the additional confusion if some parts of the program believed there was only one std::piecewise_construct
and other parts believed there were several.
Analogously, a polymorphic class can use multiple inheritance to hold many base-class subobjects of the same type Animal
; or it can use multiple virtual inheritance to hold a single virtual base-class subobject of type Animal
. But imagine the confusion if a polymorphic class inherits both virtually and non-virtually from the same type! In “dynamic_cast
From Scratch” (CppCon 2017), I used the names CatDog
and Nemo
, respectively, for the two reasonable scenarios, and SiameseCat
-with-Flea
for the confusing scenario. The MISRA-C++ coding standard explicitly bans the confusing scenario (by MISRA rule 10-1-3).
libc++ aggressively drops support for compilers older than about two years. I expect that at some point all supported compilers will permit inline constexpr
as an extension even in C++11 mode, and then libc++ will be free to add inline
to all its global variables in one fell swoop.
inline
and once without the specifier without breaking odr, even though the definitions would be efffectively the same – Poriinline
to everything that's supposed to be inline - because then I don't have to care about that keyword that's very easy to miss when I refactor things. – Catnip