With C++17 the „if constexpr“ statement was introduced. This so called “static if” or “compile-time if-expression” can be used to conditionally compile code. The feature allows to discard branches of an if statement at compile-time based on a constant expression condition.
if constexpr(condition) statement1; else statement2;
Depending on the condition the statement1 or statement2 is discarded at compile time. A discarded statement inside a template is not instantiated. Therefore, this feature is mainly used in templates. It allows to compile specific statements only, depending on the template type. This can greatly simplify template code as it will be possible to easily express intends similarly to “run-time” code. Later we will see an example where static-if is used instead of template specialization.
We already have a feature for conditionally code compilation: the “#ifdef” directive. So, will static-if replace this directive? No, it will not as these two statements are not identical. Both will conditionally compile code, but “#ifdef” will do this based on conditions that can be evaluated at preprocessing time. For example, #ifdef could not be used to conditionally compile code depending on the value of a template parameter. On the other hand, static-if cannot be used to discard syntactically invalid code, while “#ifdef” can. So, there are use cases where you can use the one or the other implementation kind and there are use cases which are specific for one of them.
Example
As mentioned before, the static-if feature is very interesting for template implementation, for example as an alternative to template specialization. The following example shows a template implementation with specific code depending on the template type.
template void PrintInfo(T x) { if constexpr (std::is_same_v) { std::cout << "string with length " << x.length() << std::endl; } else if constexpr (std::is_same_v) { std::cout << "int" << std::endl; } else { std::cout << "some other type" << std::endl; } } int main() { std::string val1 = "foo"; int val2 = 42; double val3 = 5.8; PrintInfo(val1); PrintInfo(val2); PrintInfo(val3); }
Following you will see an identical implementation static-if. In this case template specialization is used.
template void PrintInfo(T x) { std::cout << "some other type" << std::endl; } template void PrintInfo(std::string x) { std::cout << "string with length " << x.length() << std::endl; } template void PrintInfo(int x) { std::cout << "int" << std::endl; } int main() { std::string val1 = "foo"; int val2 = 42; double val3 = 5.8; PrintInfo(val1); PrintInfo(val2); PrintInfo(val3); }
If you compare the two implementations you may say that the one with template specialization is easier to read and to understand. Of course, most real implementations will be more complex so it isn’t possible to say which of the two implementation concepts is favorable. In my opinion it depends on the use case. Therefore, the static-if will not replace existing concepts like template specialization.
Compile-time discard
On the beginning of the article I mentioned that the static-if executes a conditional discard and that discarded statement inside a template will not be instantiated. What does this mean? And what difference do we have between static-if inside and outside of templates.
The following example shows the template implementation we seen previously but this time the static-if is replaced with a normal if-statement.
template void PrintInfo(T x) { if (std::is_same_v) { std::cout << "string with length " << x.length() << std::endl; } else if (std::is_same_v) { std::cout << "int" << std::endl; } else { std::cout << "some other type" << std::endl; } } int main() { std::string val1 = "foo"; int val2 = 42; double val3 = 5.8; PrintInfo(val1); PrintInfo(val2); PrintInfo(val3); }
Of course, this code is invalid and results in a compiler error as the “int” type does not have a “length” method. But in case we use static-if the example can be compiled. That’s because the discarded if-elements will not be instantiated at all.
If we use the static-if outside of a template we can see a different behavior. The discarded if-elements will be instantiated. The following code shows an easy example with an undeclared identifier within the discarded if-element. The code within the template can be compiled but the code without the template shows an according compiler error.
template void DoSomething(T x) { if constexpr(true) { std::cout << x << std::endl; } else { std::cout << y << std::endl; // OK as code is not instantiated at all } } int main() { int x = 2; DoSomething(x); if constexpr(true) { std::cout << x << std::endl; } else { std::cout << y << std::endl; // error C2065: 'y': undeclared identifier } }
static-if vs. template specialization
We already seen a comparison between static-if and template specialization within the short example at the beginning. Let’s have a look at a more complex example to get a better feeling for the differences of the concepts.
Let’s say we have the following use case. We should implement a calculation which consists of three steps: a preparation, a transformation and a result creation. The three steps are already implemented. So, we use a given interface. The preparation and result creation steps are available as type specific methods. Therefore, we want to implement the calculation function as template and use the according type specific method variants.
The following implementation contains the given interface as dummy implementation and the template implementation. The template is implemented in two variants, one uses static-if and the other one uses template specialization.
// some given interface int PrepareByString(std::string x) { return x.length(); } int PrepareByInt(int x) { return x - 10; }; void Transform(int* x) { *x = *x + 5; } std::string CreateStringResult(int x) { return std::to_string(x); } int CreateIntResult(int x) { return x + 5; } // template with constexpr template T Calculate(T x) { int temp = 0; if constexpr (std::is_same_v) temp = PrepareByString(x); else if constexpr (std::is_same_v) temp = PrepareByInt(x); else return x; Transform(&temp); if constexpr (std::is_same_v) return CreateStringResult(temp); else if constexpr (std::is_same_v) return CreateIntResult(temp); } // template with specialization template T Calculate(T x) { return x; } template std::string Calculate(std::string x) { int temp = PrepareByString(x); Transform(&temp); return CreateStringResult(temp); } template int Calculate(int x) { int temp = PrepareByInt(x); Transform(&temp); return CreateIntResult(temp); } // main function int main() { std::string val1 = "foo"; int val2 = 42; double val3 = 5.8; std::cout << Calculate(val1) << std::endl; std::cout << Calculate(val2) << std::endl; std::cout << Calculate(val3) << std::endl; }
Based in this short example we can see the advantages and disadvantages of both solutions. We have implemented a fixed calculation algorithm which is same for all data types. If we use template specialization and implement each data type separately we must duplicate this algorithm. And of course, duplicated code comes with the well-known disadvantages. By using static-if we must implement the algorithm one time only. But we had to add two if-statements. So instead of the straight procedure we add code branches. Therefore, the complexity of the single calculation method increases but the complexity of the template is reduced as the template contains one method only instead of three methods.
This is still an easy example with a few lines of code, but it will show the main difference of the concepts and the resulting code. Whether you use the one or the other concept may be use case specific. And of course, there are many other alternatives too or you can even mix up several concepts. In summary I recommend use of static-if in templates. It often improves the code quality as it makes the source code easier to read and to maintain.
Nice. But I think all your std::is_same_v expressions are missing their type parameters.