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 else
s) 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
How Can Std::Make_Heap Be Implemented While Making at Most 3N Comparisons
Details of Std::Make_Index_Sequence and Std::Index_Sequence
How to Alloc a Executable Memory Buffer
Is There a Portable Equivalent to Debugbreak()/_Debugbreak
Does Casting to an Int After Std::Floor Guarantee the Right Result
How to Cancel/Detach a Future in C++11
Is Volatile Bool for Thread Control Considered Wrong
Convert C++ Function Pointer to C Function Pointer
How to Make a Portable Isnan/Isinf Function
Universal Less<> for Pointers in C++ Standard
Operator= and Functions That Are Not Inherited in C++
When Is a Type in C++11 Allowed to Be Memcpyed
Can Class Template Constructors Have a Redundant Template Parameter List in C++20
What Happens If I Read a Map's Value Where the Key Does Not Exist