Search code examples
c++templatesconstexprif-constexprguard-clause

constexpr guard clause does not compile


I wanted to add a constexpr guard clause in my code in order to avoid unnecessary indentation, but ran into this issue.

This compiles:

#include <string>
#include <iostream>

template<class T>
void inc(T& t) {
    if constexpr (!std::is_arithmetic_v<T>) {
        return;
    } else {
        ++t;
    }
}

int main() {
    int i = 1;
    std::string s = "bar";
    inc(i);
    inc(s);
    std::cout << "Success!";
}

Whereas this does not:

#include <string>
#include <iostream>

template<class T>
void inc(T& t) {
    if constexpr (!std::is_arithmetic_v<T>) {
        return;
    }
    ++t;
}

int main() {
    int i = 1;
    std::string s = "bar";
    inc(i);
    inc(s);
    std::cout << "Success!";
}

Compiler error:

main.cpp:9:5: error: cannot increment value of type 'std::string'
    ++t;
    ^ ~
main.cpp:17:5: note: in instantiation of function template specialization 'inc<std::string>' requested here
    inc(s);
    ^
1 error generated.

Why won't this work?

To clarify: I'm writing a googletest TYPED_TEST to test my code for various types, but some tests requires the type to be incrementable. I thought to add a constexpr guard clause to skip types that does not support that operation.


Solution

  • An if constexpr branch that is taken and unconditionally exits the function doesn't prevent a check of the validity of substituted expressions following the if constexpr statement, for the same reason that a simple unconditional return statement doesn't prevent the same check.

    Of course, theoretically, if there is a statement that unconditionally exits from a function one could argue that it doesn't matter what comes after that statement and that it doesn't matter either whether substitution of template arguments results in a valid expression.

    However, there is no such rule. A statement following an unconditional exit is still checked for validity after substitution in the same way any other non-discarded statement is checked.

    If there was such a rule, it would need to define exactly when validity of statements isn't checked. In general it is undecidable (in the sense of computability theory) whether a statement in a function is reachable. So at best you'd need to specify special cases such as a separate return statement in the same scope or a nested if constexpr branch as condition for the special rule to apply.

    But then you'd have a weird difference depending on the exact form in which the function is written. I don't think adding this complexity would be beneficial overall. Also, there are other effects of just having the substituted statements, even if they are unreachable, e.g. regarding odr-use and implicit instantiation of template specializations, which would then also have different behaviour depending on the exact syntactical form of the function body.

    There is also no need for such a rule, because it can in practice always be achieved explicitly with a if constexpr. In your case the condition can be negated and then ++t can be placed into the true-branch to simplify the syntax a bit. Otherwise, the else branch can be taken. That this requires some extra punctuation and indentation is unlikely to be good enough reason to add complexity to the language. C++ isn't known to be very concise in syntax anyway.