Search code examples
c++language-lawyerc++23

Why does C++23 if consteval not allow different return types as a similar if constexpr would do?


introduces if consteval, which is, as far as I understand, more or less a shortcut for 's std::is_constant_evaluated

// C++20
if (std::is_constant_evaluated())
{
}
else
{
}

// C++23
if consteval
{
}
else
{
}

A consequence of this is that the following does not compile:

[[nodiscard]] constexpr auto bar()
{
    if consteval
    {
        return 2;
    }
    else
    {
        return "bar"; // clang: "inconsistent deduction for auto return type: 'int' and then 'const char*", analogue diagnostics from gcc and MSVC
    }
}

This makes sense, because the following snippet doesn't compile either for the same obvious reason:

#include <type_traits>

[[nodiscard]] constexpr auto bar()
{
    if(std::is_constant_evaluated())
    {
        return 2;
    }
    else
    {
        return "bar"; // Same diagnostic as above
    }
}

However, in the case of if constexpr, one is allowed to write things like the snippet below, which does compile:

template<bool b>
[[nodiscard]] constexpr auto bar()
{
    if constexpr(b)
    {
        return 2;
    }
    else
    {
        return "bar";
    }
}

My question is then:

What forbids such a thing in the Standard (or what makes it impossible)?

I assume this was to keep the same behaviour as std::is_constant_evaluated, but again, what says this is the correct behaviour, and not something akin to the if constexpr version?

DISCLAIMER :

  • I am not interested in workarounds. I'm interested in what forbids such a thing in the Standard (or what makes it impossible). I know that in this particular case, one obvious workaround is to specify the return type to be something like std::variant<int, const char*>, or any similar construct.

  • I am not arguing the first snippet in this question should compile either. I'm not sure if I want it to, actually.


Solution

  • It just makes it very confusing for the type system. Consider this:

    constexpr auto f() {
        if consteval {
            return 1;
        } else {
            return 0;
        }
    }
    
    constexpr int (*fp)() = &f;
    
    int main() {
        constexpr int i = fp();
        int j = fp();
        static_assert(i == 1);
        assert(j == 0);
    }
    

    If the return types were different in each branch what would the function type be?

    Sure, you could theoretically come up with a syntax for a function type that is dependent on if the call is constant-evaluated. Or something else like banning function pointers to such functions. Something like that just doesn't exist today.


    As for what wording makes if consteval behave differently from if constexpr, it's actually because if constexpr and deduced placeholder types have a special interaction:

    [stmt.if]p2:

    If the value of the converted condition is false, the first substatement is a discarded statement, otherwise the second substatement, if present, is a discarded statement.

    [dcl.spec.auto.general]p3:

    If the declared return type of the function contains a placeholder type, the return type of the function is deduced from non-discarded return statements, if any, in the body of the function ([stmt.if]).

    And no such wording exists for if consteval. So both return statements are used to deduce the return type, which fails because they are of different types.


    BTW, if consteval does more than just if (std::is_constant_evaluated()):

    consteval int f(int x) { return x*x; }
    
    constexpr void g(int x) {
        if consteval {
            f(x);  // OK
        }
        if (std::is_constant_evaluated()) {
            f(x);  // Error: consteval function call does not form a constant expression and is not inside an immediate function or consteval if
        }
    }