I am trying to dig into implications of a function being inline
and stumbled upon this issue. Consider this small program (demo):
/* ---------- main.cpp ---------- */
void other();
constexpr int get()
{
return 3;
}
int main()
{
std::cout << get() << std::endl;
other();
}
/* ---------- other.cpp ---------- */
constexpr int get()
{
return 4;
}
void other()
{
std::cout << get() << std::endl;
}
When compiled without optimizations, the program yields the following output:
3
3
Which might be not what we want, but at least I can explain it.
constexpr
functions on compile time, so it decided to postpone it to runtime. constexpr
on functions implies inline
get()
functions happened to have different implementationsget()
functions staticget()
functionAnd it so happened that the linker chose get()
from main.cpp
, which returned 3.
Now to the part I don't get. I simply changed get()
functions from constexpr
to consteval
. Now the compiler is required to compute the value during compile time, i.e. before the link time (right?). I'd expect get()
functions not to be present in object files at all.
But when I run it (demo), I have exactly the same output! How can this be?.. I mean yes, I understand that this is undefined behavior, but this is not the point. How come that the values that should have been computed on compile time interfered with other translation unit?
UPD: I am aware that this feature is listed as unimplemented in clang, but the question is applicable anyway. Is a conformant compiler allowed to exhibit such a behavior?
A program with two definitions of the same inline function is an ill-formed program, no diagnostics required.
The standard places no requirements on the runtime or compile-time behavior of an ill-formed program.
Now, there is no "compile time" in C++ as you are imagining it. While almost every C++ implementation compiles files, links them, builds a binary, then runs it, the C++ standard tip-toes around this fact.
It talks about translation units, and what happens when you put them together into a program, and what that program's runtime behaviour is.
...
In practice, your compiler could be building a map from symbol to some internal structure. It is compiling your first file, and then in the second file it is still accessing that map. A new definition of the same inline function? Just skip it.
Second, your code must produce a compile time constant expression. But a compile time constant expression is not an observable property in the context where you used it, and there are no side effects to doing it at link or even run time! And under as-if there is nothing preventing that.
consteval
is saying "if I run this and the rules that permit it being a constant expression are violated, I should error rather than fall back on non-constant expression". This is similar to "it must be run at compile time", but it is not the same.
To determine which of these is happening, try this:
template<auto x>
constexpr std::integral_constant< decltype(x), x > constant = {};
now replace your print lines with:
std::cout << constant<get()> << std::endl;
this makes putting off the evaluation to run/link time as impractical.
That will distinguish between the "compiler is being clever and caching get
" from "compiler is evaluating it later at link time", because determining which ostream& <<
to call requires instantiating the type of constant<get()>
, which in turn requires evaluating get()
.
Compilers tend not to defer overload resolution to link time.