Difference Between "If Constexpr()" VS "If()"

Difference between if constexpr() Vs if()

The only difference is that if constexpr is evaluated at compile time, whereas if is not. This means that branches can be rejected at compile time, and thus will never get compiled.


Imagine you have a function, length, that returns the length of a number, or the length of a type that has a .length() function. You can't do it in one function, the compiler will complain:

template<typename T>
auto length(const T& value) noexcept {
if (std::integral<T>::value) { // is number
return value;
else
return value.length();
}

int main() noexcept {
int a = 5;
std::string b = "foo";

std::cout << length(a) << ' ' << length(b) << '\n'; // doesn't compile
}

Error message:

main.cpp: In instantiation of 'auto length(const T&) [with T = int]':
main.cpp:16:26: required from here
main.cpp:9:16: error: request for member 'length' in 'val', which is of non-class type 'const int'
return val.length();
~~~~^~~~~~

That's because when the compiler instantiates length, the function will look like this:

auto length(const int& value) noexcept {
if (std::is_integral<int>::value) { // is number
return value;
else
return value.length();
}

value is an int, and as such doesn't have a length member function, and so the compiler complains. The compiler can't see that statement will never be reached for an int, but it doesn't matter, as the compiler can't guarantee that.

Now you can either specialize length, but for a lot of types (like in this case - every number and class with a length member function), this results in a lot of duplicated code. SFINAE is also a solution, but it requires multiple function definitions, which makes the code a lot longer than it needs to be compared to the below.

Using if constexpr instead of if means that the branch (std::is_integral<T>::value) will get evaluated at compile time, and if it is true then every other branch (else if and else) gets discarded. If it is false, the next branch is checked (here else), and if it is true, discard every other branch, and so on...

template<typename T>
auto length(const T& value) noexcept {
if constexpr (std::integral<T>::value) { // is number
return value;
else
return value.length();
}

Now, when the compiler will instantiate length, it will look like this:

int length(const int& value) noexcept {
//if (std::is_integral<int>::value) { this branch is taken
return value;
//else discarded
// return value.length(); discarded
}

std::size_t length(const std::string& value) noexcept {
//if (std::is_integral<int>::value) { discarded
// return value; discarded
//else this branch is taken
return value.length();
}

And so those 2 overloads are valid, and the code will compile successfully.

if vs if constexpr inside constexpr function

Before c++17, we don't have if constexpr, so the choice is if, which means it is not guaranteed to get our constexpr functions get evaluted at compile time, all depend on compiler implementation

The fact that an if statement is not constexpr does not mean it can't be evaluated at compile time, as part of a constexpr expression. In your example, v is evaluated at compile time in both cases, because it is required to be: it's a constant expression. That's not implementation defined.

After c++17, if constexpr is prefered if we want constexpr functions get evaluated at compile time.

Constexpr if statements were introduced to solve a problem. Getting constexpr functions to get evaluated at compile time is not that problem.

Here is an example where a constexpr if is required instead of a simple if (taken from cppreference):

template <typename T>
auto get_value(T t) {
if constexpr(std::is_pointer_v<T>)
return *t; // deduces return type to int for T = int*
else
return t; // deduces return type to int for T = int
}

Try removing the constexpr keyword and see what happens (demo).

Also, note that you can always solve that problem using other methods, but if constexpr has the advantage of conciseness. For instance, here's an equivalent get_value using tag dispatching:

template<typename T>
auto get_value_impl(T t, std::true_type) {
return *t;
}
template<typename T>
auto get_value_impl(T t, std::false_type) {
return t;
}

template<typename T>
auto get_value(T t) {
return get_value_impl(t, std::is_pointer<T>{});
}

Demo

#if Vs if constexpr

Note that, if if constexpr is not part of a template, then the other parts of the if (such as the elses) are still compiled and checked for validity.

From cppreference:

If a constexpr if statement appears inside a templated entity, and if condition is not value-dependent after instantiation, the discarded statement is not instantiated when the enclosing template is instantiated .

Outside a template, a discarded statement is fully checked. if constexpr is not a substitute for the #if preprocessing directive:

`if constexpr` vs `if` in light of compiler optimization and code performance

if constexpr is not intended about optimization. Compilers are very good at optimizing away a branch that is if (true) or if (false) (since we're talking about constant expressions, that is what it boils down to). Here is a godbolt demo of the example in OP - you'll note that both gcc and clang, even on -O0, do not emit a branch for a simple if.

if constexpr is all about ensuring that only one branch of the if is instantiated. This is hugely important and valuable for writing templates - because now we can actually write conditionally compiling code within the body of the same function instead of writing multiple artificial functions just to avoid instantiation.

That said, if you have a condition that is a known constant expression - just always use if constexpr, whether or not you need the instantiation benefit. There is no downside to such a decision. It makes it clearer to readers that indeed this condition is constant (since otherwise it wouldn't even compile). It will also force the evaluation of the expression as a constant (a slight variant leads gcc to emit a branch at -O0, thought not at -O1), which with the coming addition of is_constant_evaluated() may become more important in the long run (possibly even negating my opening paragraph).




The only advantage I see would be to tell the programmer explicitly that this if is compile-time; however, I would say the conditional expression is self-explanatory.

To address this specifically, yes, std::is_same<X, Y>::value is "self-explanatory" that it is a constant expression... because we happen to be familiar with std::is_same. But it's less obvious whether foo<X>::value is a constant expression or whether foo<X>() + bar<Y>() is a constant expression or anything more arbitrarily complicated than that.

It's seeing if constexpr that makes the fact that it's compile-time self-explanatory, not the content of the condition itself.

Conditional constexpr on non-dependent condition

You can silence the compiler diagnostics by ensuring that there is some set of template parameters and function arguments for which a call to your function is a constant expression even if ::iterations() is not a constant expression. For example:

template <bool True = true>
constexpr std::uint32_t algorithm(std::uint32_t x) {
if constexpr (True) {
auto const iterations = ::iterations();

for (int i = 0; i < iterations; ++i) {
x *= x;
}

return x;
} else {
return 0;
}
}

algorithm<false>(meow) is a constant expression whenever meow is, so a compiler cannot complain (https://godbolt.org/z/GvE9ME).

Use of constexpr function before definition fails

A constexpr function does NOT have to be defined before its first use, however the result of any call made prior to definition is not a constant expression.

Source: C++ Standard draft n4296, section 5.20:

A conditional-expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine, would evaluate one of the following expressions:

  • this, except in a constexpr function or a constexpr constructor that is being evaluated as part of e;
  • an invocation of a function other than a constexpr constructor for a literal class, a constexpr function, or an implicit invocation of a trivial destructor [ Note: Overload resolution is applied as
    usual — end note ];
  • an invocation of an undefined constexpr function or an undefined constexpr constructor;
  • ...

version from draft 3485 (section 5.19):

A conditional-expression is a core constant expression unless it involves one of the following as a potentially evaluated subexpression, but subexpressions of logical AND, logical OR, and conditional operations that are not evaluated are not considered [ Note: An overloaded operator invokes a function. — end note ]:

  • this [ Note: when evaluating a constant expression, function invocation substitution replaces each occurrence of this in a constexpr member function with a pointer to the class object. — end note ];
  • an invocation of a function other than a constexpr constructor for a literal class or a constexpr function [ Note: Overload resolution is applied as usual — end note ];
  • an invocation of an undefined constexpr function or an undefined constexpr constructor
  • ...

The example int x2 = s. t(); in n2235 actually became valid due to the changes made prior to Standardization. However, constexpr int x2 = s. t(); remains an error.

Function is not usable as a 'constexpr' function

In order to evaluate this:

  if constexpr (p(x)) {

We need for p(x) to be a constant expression. The rules for whether something qualifies as a constant expression or not are based on a list of things that you're not allowed to do.

When x is a basic_type<int> and p is a function that takes a basic_type<int> by value, there are simply no rules that we are violating. This is an empty type, so copying it (as we're doing here) doesn't actually involve any kind of read. This just works.


But when x is an int and p is a function that takes an int by value, this also requires copying x but this time it involves reading the value of x. Because, of course, gotta initialize the parameter somehow. And this does run afoul of a rule: [expr.const]/8 says we're not allowed to perform:

an lvalue-to-rvalue conversion unless it is applied to

  • a non-volatile glvalue that refers to an object that is usable in constant expressions, or
  • a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of E;

An lvalue-to-rvalue conversion is what happens when we read the value of a variable, and neither of those bullets apply. It doesn't matter here that you don't actually care what the value is, since p doesn't use it. In order to be able to even invoke p, you have to copy x, and you're not allowed to do that. Hence the error.


However, your lambda here doesn't actually need the value, just the type, so you could instead write this:

    return foo(
[]<typename T>(T const&)
{
return std::is_integral_v<std::decay_t<T>>;
},
0);

Now we're no longer copying x into the lambda, since the lambda no longer takes by value - it takes by reference. As a result, we're not violating the lvalue-to-rvalue conversion rule (or any other rule) and this is now a valid constant expression.


Then, as a bonus, if you change foo to take x by reference (because, again, you don't actually care about the value, so why not):

consteval auto foo(auto p, auto const& x) noexcept {
if constexpr (p(x)) {
return 1;
} else {
return 0;
}
}

Then both variants become ill-formed. Both the basic_type<int> and int versions (regardless of whether you take the int by value or by reference). For more on this case, see the constexpr array size problem which I'm currently trying to resolve with P2280.

constexpr if vs if with optimizations - why is constexpr needed?

This is easy to explain through an example. Consider

struct Cat { void meow() { } };
struct Dog { void bark() { } };

and

template <typename T>
void pet(T x)
{
if(std::is_same<T, Cat>{}){ x.meow(); }
else if(std::is_same<T, Dog>{}){ x.bark(); }
}

Invoking

pet(Cat{});
pet(Dog{});

will trigger a compilation error (wandbox example), because both branches of the if statement have to be well-formed.

prog.cc:10:40: error: no member named 'bark' in 'Cat'
else if(std::is_same<T, Dog>{}){ x.bark(); }
~ ^
prog.cc:15:5: note: in instantiation of function template specialization 'pet<Cat>' requested here
pet(Cat{});
^
prog.cc:9:35: error: no member named 'meow' in 'Dog'
if(std::is_same<T, Cat>{}){ x.meow(); }
~ ^
prog.cc:16:5: note: in instantiation of function template specialization 'pet<Dog>' requested here
pet(Dog{});
^

Changing pet to use if constexpr

template <typename T>
void pet(T x)
{
if constexpr(std::is_same<T, Cat>{}){ x.meow(); }
else if constexpr(std::is_same<T, Dog>{}){ x.bark(); }
}

only requires the branches to be parseable - only the branch that matches the condition needs to be well-formed (wandbox example).

The snippet

pet(Cat{});
pet(Dog{});

will compile and work as expected.

if constexpr vs if with constant

A bit contrived example, but consider this:

const int foo = 6;
if (foo == 5)
{
some_template_that_fails_to_compile_for_anything_else_than_5<foo>();
}

This will not compile even though the body of the if will never be executed! Still the compiler has to issue an error. On the other hand, this

const int foo = 6;
if constexpr (foo == 5)
{
some_template_that_fails_to_compile_for_anything_else_than_5<foo>();
}

is fine, because the compiler knows at compile time the value of foo and hence does not bother about the body of the if.

Function with parameter pack with sizeof ... (args) == 0 as base case doesn't compile

Even if

if (sizeof ... (args) == 0)

the entire function must be well-formed C++.

    return std::max(n, mx(args ...));

This still must be valid C++, even if won't get executed. If, outside of template context, you have an if (1), the else part must still be valid C++, you can't just throw randomly-generated gibberish in there, and this is the same thing. And when sizeof...(args) is 0 the function call becomes mx() and that, of course, has no valid overload.

What you want to do, instead, is use if constexpr instead of your garden-variety if.

if constexpr (sizeof ... (args) == 0)


Related Topics



Leave a reply



Submit