5

Suppose you have a function template that calls another function, which may or may not be a constexpr function depending on a template parameter. C++ allows the callee to be declared constexpr anyway, as long as at least one possible instantiation produces a constexpr.

For example:

template <class T> constexpr bool bar() { return true; } template <> bool bar<int>() { return false; } template <class T> constexpr bool foo() { return bar<T>(); } foo<short>(); foo<int>(); // not a constexpr, but compiles anyway 

This allows a nice degree of flexibility so that we can produce constexpr function calls whenever possible, but fall back to a non-constexpr otherwise.

However, I notice the same flexibility does not extend to the C++17 if constexpr.

For example:

if constexpr(foo<short>()) { /* do something */ }; // works if constexpr(foo<int>()) { /* do something */ }; // won't compile, bar<int>() is not constexpr! 

I've encountered situations where I'd like to use if constexpr to avoid the compilation time overhead of instantiating certain templates, but the evaluated expression may not always be a constexpr depending on template parameters. Is there some reason that if constexpr doesn't just "degrade" to a non-constexpr if statement if the conditional expression depends on a template parameter and the template instantiates to a non-constexpr? Just like with the behavior of constexpr functions?

Is this simply an arbitrary omission in the standard (i.e. nobody thought it would be useful), or is there some more fundamental reason why if constexpr can't "degrade" to a non-constexpr if?

3 Answers 3

8

It doesn't "degrade" for the same reason that this doesn't degrade:

constexpr auto value = expression; 

If you declare a variable to be constexpr, then you mean it. You mean that its value is a compile-time constant, and the compiler will perform constant evaluation to generate its value.

The same goes for if constexpr; the condition is a constant expression. if constexpr exists to choose between different pieces of code based on whether a particular constant expression yields certain values. It has special discarding mechanics that allow ill-formed code to exist in conditions not taken under certain circumstances.

There is no "degrading" here because there's not supposed to be. The question really is not why if constexpr can't "degrade"; it's why function constexpr does "degrade". It's function-level constexpr that's the odd one out, and that oddity is why we had to invent a whole new keyword in C++20 to mean "yes, I really definitely mean that this function is a constant expression".

Sign up to request clarification or add additional context in comments.

5 Comments

Totally agree. I've always found the "fallback" mechanics to be troublesome.
@NathanOliver: I don't necessarily agree with that. I don't think it would be useful for if constexpr, but it's very useful for function constexpr. The problem is not that we have a mechanism with fallbacks; it's that both the "degrading" and non-degrading version are spelled the same way (except for when they aren't thanks to consteval).
Agreed, the issue is just the syntax really. The semantics of both constructs is useful on their own.
In C++17 lambdas are implicitly constexpr if the rules allow. Why does this not work for functions? Why was this keyword introduced for functions at all? (Should I rather ask this as a question?)
@Dr.Gut: "Why does this not work for functions?" Define "work". How well does it "work" for lambdas? It certainly doesn't give me a compile error if I do something that isn't permitted in constexpr code; it just makes my lambda not implicitly constexpr. How do I tell the compiler that I mean for my code to be able to run at compile-time? The lambda implicit rule is a function of not having a place syntactically to put it, as well as wanting to keep C++'s lambda syntax from being even more verbose.
5

The keyword constexpr means very different things in these 2 contexts:

constexpr void f(); 

and

if constexpr(expr) 

In the case of f, the constexpr means that f may be evaluated at compile time. But it's perfectly fine to call f at run-time as well.

In the case of the if constexpr, the expression expr must be an expression that can be evaluated at compile time.

So in the case of the function f, it makes sense to "degrade" the constexpr-ness so that the function can be called at run-time, but this doesn't make sense in the context of the constexpr if.

I suggest thinking of these 2 forms as unrelated to each other (think of the constexpr keyword occuring in both cases as being a coincidence). If you are aware of the consteval keyword, then you can think of if constexpr as really being if consteval since the expression must be evaluated at compile time, similar to a consteval function.

2 Comments

Yeah, I understand that... I'm asking why exactly that is. Why can't if constexpr simply "degrade" to a regular if statement if expr can't be evaluated at compile time?
@Siler It's just not designed to do that. I can't think of a reason off the top of my head that disallows such a construct. Not sure how it would be used though. What happens to the discarded branch if it degrades to a run-time check?
0

If both branches of an if compile, you can use just a regular if. If the compiler can prove that the condition always (or never) holds, it can elide one branch (although it still must instantiate everything needed for the other). This can include non-constant expressions, like i>1 || i<2, but might or might not include any given constant expression because the compiler isn’t required to fully expand everything so as to check (and optimization is optional anyway).

In the unlikely event that somehow you know that the wrong branch will compile except perhaps in a subset of the circumstances in which the condition will be a constant expression (with the correct value!), you can use SFINAE to detect that:

template<class T,class=int[void(T::maybe()),1]> constexpr bool use_maybe(int) {return true;} template<class> constexpr bool use_maybe(long) {return false;} template<class T> void client() { if(T::maybe()<4) { // non-constant values are bigger if constexpr(use_maybe<T>(1)) { int buf[T::maybe()]; T::use(buf); } else std::abort(); } else { std::vector<int> buf(T::maybe()); T::use(buf.data()); } } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.