What is the difference between if constexpr()
and if()
?
Where and When can I use both of them?
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::is_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::is_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 constexpr (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 constexpr (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.