"Constexpr If" VS "If" with Optimizations - Why Is "Constexpr" Needed

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` 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.

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.

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:

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.

constexpr if and the return value optimization

This has nothing to do with if constexpr

Simply this code is not allowed to compile:

class A {
public:
A(A const &) = delete;
explicit A(int);
};

A test(int a)
{
A x{a};
return x; // <-- error call to a deleted constructor `A(A const &) = delete;`
}

The changes in C++17 you are thinking of have to do with temporary materialization and don't apply to NRVO because x is not a prvalue.

For instance this code was illegal pre C++17 and now it is allowed:

A test(int a)
{
return A{a}; // legal since C++17
}

Why isn't constexpr guaranteed to run during compilation?

constexpr already guarantees compile-time evaluation when used on a variable.

If used on a function it is not supposed to enforce compile-time evaluation since you want most functions to be usable at both compile-time and runtime.

consteval allows forcing functions to not be usable at runtime. But that is not all that common of a requirement.

Why does constexpr context make the compiler fail, while w/o it optimizes perfectly?

According to dcl.constexpr

For a constexpr function or constexpr constructor that is neither defaulted nor a template, if no argument values exist such that an invocation of the function or constructor could be an evaluated subexpression of a core constant expression, or, for a constructor, an evaluated subexpression of the initialization full-expression of some constant-initialized object ([basic.start.static]), the program is ill-formed, no diagnostic required.

As memcpy is not constexpr, your program is ill formed NDR.

Using the function in contsexpr context would allow to have diagnostic.

In some situations adding constexpr in front of a function enables GCC to try optimizing harder which results in fully optimizing the function away and just providing the calculated value.

It is a good hint (as inline before).

constexpr function can be "misused":

constexpr std::size_t factorial(std::size_t n) {/*..*/}

int main()
{
std::cout << factorial(5); // computed at runtime (but probably optimized)
}

Correct way would be

int main()
{
constexpr auto fact5 = factorial(5); // computed at compile time
std::cout << fact5;
}


Related Topics



Leave a reply



Submit