Can consteval functions from different translation units interfere?

527 Views Asked by At

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.

  1. The compiler is not required to compute results of constexpr functions on compile time, so it decided to postpone it to runtime.
  2. constexpr on functions implies inline
  3. Our get() functions happened to have different implementations
  4. We did not declare get() functions static
  5. The linker must chose only one implementation of the get() function

And 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?

3

There are 3 best solutions below

4
On BEST ANSWER

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.

1
On

The requirement for a consteval function is that every call to it must produce a constant expression.

Once the compiler satisfies itself that a call does produce a constant expression, there's no requirement that it must not codegen the function and call it at run time. Of course, for some consteval functions (like those envisioned for reflection) it had better not do that (at least if it doesn't want to put all its internal data structures into the object file), but that's not a general requirement.

Undefined behavior is undefined.

0
On

The answer to this is that it's still an ODR violation, no matter if the function is a constexpr or consteval. Maybe with a particular compiler and a particular code you may get the answer you're expecting, but it's still ill formed, no diagnostic required.

What you could do is to define them in anonymous namespaces:

/* ---------- main.cpp ---------- */
void other();

namespace {
    constexpr int get()
    {
        return 3;
    }
}

int main() 
{
    std::cout << get() << std::endl;
    other();
}

/* ---------- other.cpp ---------- */
namespace {
    constexpr int get()
    {
        return 4;
    }
}

void other()
{
    std::cout << get() << std::endl;
}

But even better, simply use modules:

/* ---------- main.cpp ---------- */
import other;

constexpr int get()
{
    return 3;
}

int main() 
{
    std::cout << get() << std::endl; // print 3
    other();
}

/* ---------- other.cpp ---------- */
export module other;

constexpr int get() // okay, module linkage
{
    return 4;
}

export void other()
{
    std::cout << get() << std::endl; // print 4
}