Search code examples
c++runtimecode-coverageconstexprside-effects

Constexpr functions not called at compile-time if result is ignored


I was investigating some rather weird code-coverage results of constexpr functions (compile-time calls can't get instrumented by the code-coverage tool I use), and noticed that some constexpr functions were getting called as runtime functions, if the results of the function call were not stored.

It seems that, for constexpr functions or methods, if you store the results of the call (either in a runtime variable [emphasis!!!] or a constexpr variable), the call is a compile-time call (as long as the parameters are compile-time). If you ignore the results, the call is a runtime call. This has nothing to do with my code-coverage tool; the behavior seems to be repeatable in the simple example below.

You could argue that since constexpr functions cannot have side-effects, it doesn't matter what the compiler does if you don't return / use a result. I'd think that for efficiency, the compiler would still make whatever it can be constexpr, but that's neither here nor there... What I'm wondering is if this is even defined behavior.

Is this a portable way to guarantee your constexpr functions will be invoked as runtime??? There aren't a ton of uses for that, but one use is: if you want to "take credit for tests that were called on constexpr functions" in code coverage, by just calling the same function at the end of your unit test, and ignoring the result, so that they get instrumented.

There are other ways to force a function to get called as runtime, I know, I'm mostly curious about what's going on here. It was very unexpected when I first saw it. Unless this is portable, I'll probably just call my constexpr methods through a runtime object (which seems to do the trick even for static methods), or through a runtime function pointer.

See example below. Live demo: https://onlinegdb.com/rJao0RNGP

// Modified from https://stackoverflow.com/a/40410624/12854372

extern bool no_symbol;

struct ContextIsConstexpr {
    size_t s;

    constexpr ContextIsConstexpr() : s(1) {}
    
    constexpr void operator()() {
        auto ignore = s ? 1 : no_symbol;
    }
};

constexpr bool tryIgnoringResult()
{
    ContextIsConstexpr()();
    return true;
}

constexpr void thereIsNoResult() {
    ContextIsConstexpr()();
}

int main()
{
    constexpr auto result1 = tryIgnoringResult(); // OK, compile-time
    auto result2 = tryIgnoringResult(); // OK, compile-time

    // tryIgnoringResult(); // Unexpected, runtime!
    // thereIsNoResult(); // Unexpected, runtime!
}

Solution

  • It might be confusing, but constexpr functions should be called at compile time only in constexpr contexts (assignation to constexpr variable, use for array size or template parameter, ...).

    In regular context, functions are called at runtime. Optimizer might resolve that function at compile time (as for any other functions following the as-if rule). constexpr is indeed a good hint for optimization to happen.

    You could argue that since constexpr functions cannot have side-effects

    They can have side effect, see following valid example:

    constexpr int f(int i)
    {
        if (i == 0) return 0;
        std::cout << i << std::endl;
        return i;
    }
    
    int main()
    {
        [[maybe_unused]] constexpr int zero = f(0); // Compile time
        f(42); // runtime
    }
    

    Demo