Search code examples
c++reflectionmetaprogrammingcompile-timeif-constexpr

if constexpr Seems to Only Work if Both Cases are Valid


Given the following code:

template<typename T>
constexpr remove_reference_t<decltype(T{}.x, bool{})> has_x() {return true;}
template<typename T, class... U>
constexpr bool has_x(U...) {return false;}

class A { public: int x; };

int main()
{
    vector<int> vec;
    A my_a{};

    std::cout << has_x<decltype(my_a)>() << endl << has_x<decltype(vec)>() << endl;

    if constexpr(has_x<decltype(vec)>())
    {
        cout << vec.x << endl;
    }
    else
    {
        cout << size(vec) << endl;
    }
}

It compiles only if I comment out the cout << vec.x << endl. This obviously won't compile, but my understanding from if constexpr was that:

If the value is true, then statement-false is discarded (if present), otherwise, statement-true is discarded

Thus I thought that "statement-true" should be discarded, but this doesn't seem to be the case. If I put a statement that's valid in either case in the "statement-true" it works. But with a potentially invalid statement I get:

error: class std::vector<int> has no member named x

Am I doing something wrong here?

Live Example


Solution

  • With a constexpr if, the body of the discarded statement must still be syntactically correct, even if it is not compiled. The compiler knows at compile time that

    vec.x
    

    is incorrect, so you get an error. If you refactor the code to use a template like

    template<typename T>
    void foo(T& vec)
    {
        if constexpr(has_x<T>())
        {
            cout << vec.x << endl;
        }
        else
        {
            cout << size(vec) << endl;
        }
    }
    
    int main()
    {
        vector<int> vec;
        A my_a{};
    
        std::cout << has_x<decltype(my_a)>() << endl << has_x<decltype(vec)>() << endl;
        foo(vec);
    
    }
    

    then

    vec.x
    

    is syntactically correct, it doesn't know what vec is, but it is not malformed code, so it passes. Then, once the template is instantiated, the condition of the if statement is evaluated and the vec.x is discarded so there is no compiler error.


    Typically you only use a constexpr if in template contexts. You don't have to, but if you don't you have to make sure that the body will compile at compile time, even if it would be discarded, just like a plain old if statement.


    Even inside templates you still have to be careful. If the body of the constexpr if does not rely on the template parameter, then it will be evaluated before the template is ever instantiated. Using

    template <typename T>
    void f() 
    {
         if constexpr (std::is_integer_v<T>)
             // ...
         else
           static_assert(false, "T must be an integer type"); 
    }
    

    the code won't compile since static_assert(false, "T must be an integer type") triggers when the template is parsed. You have to make the condition depend on the template type so it will be evaluated at instantiation time like

    template<class T> struct always_false : std::false_type {};
    
    template <typename T>
    void f() 
    {
         if constexpr (std::is_integer_v<T>)
             // ...
         else
           static_assert(always_false<T>, "T must be an integer type"); 
    }