Hello again! In this video, we are going to look at the constexpr if statement.
This came in, in C++17.
If you are reading discussions from that time, you may see it referred to as "if constexpr", or "static
if".
And this was one of the most requested changes to C++.
And in this video I will try and explain why it was so requested.
As the name suggests, constexpr if allows conditionals to be evaluated at compile time. As
opposed to the plain "if" statements which is evaluated
while the program is running.
So this allows us to write expressions like this.
if constexpr "a" less than "b". And, provided that "a" and "b" are constant expressions,
this will be evaluated at compile time.
So if "a" is less than "b", then the compiler will evaluate this code, and compile it into the program.
And if "a" is not less than B said, the compiler will evaluate this code and compile it into the program.
So the interesting point here is that only the branch which is taken gets included in the program.
The other branches are ignored and do not appear in the program's source code. As seen by the compiler.
So this means we can use constexpr if for doing conditional compilation.
We were already able to do conditional compilation in C++, using the preprocessor directives. Which
came from C.
We have the directives
"#if" and
"#ifdef". And these can be used for conditionally including or excluding code from the compilation.
But there are a number of drawbacks to these, which we touched on in the overview of this section.
These directives are processed by the preprocessor, which does not understand anything about C++.
So it does not understand types or C++ syntax.
It can only do simple text based substitution.
It does not evaluate any arguments.
So we can have problems involving operator precedence, and expressions with side effects. With constexpr
if, this is executed by the compiler, during the compilation. So it has access to everything that
the compiler knows about C++ and the program. In particular, it knows about type information.
As an example, we are going to use a function which returns a string. It can obtain the string either
from a library string, in which case it just returns the string directly. Or from a built-in type,
in which case it calls the to_string() library function, which will return the string containing the same
digits as the argument.
We have to use a conditional because the to_string() function does not have an overload which takes a
string argument.
So if we try to pass a string to this, we would get a compiler error.
So we need to know the type of the data that we are using for the string.
So we would have some pseudocode, which looks like this.
If the argument is a string, then return it directly.
Otherwise, call to_string(), and then return whatever that gives us.
And then we are going to write this as a template function, which checks the argument's type traits.
And if you are not too sure about that, or the syntax, do not worry.
This is just an example.
So we could have a function which looks like this with the usual C++ if statements.
Then we have this bit of template
metaprogramming code.
Do not worry about this.
It just means that, if the parameter is a string, then this will be replaced by "true".
And if the parameter is not a string, then this will be replaced by "false", when the template is instantiated.
So let's try this out.
So here is our template function with the if statement.
We have some code which calls it. First of all, we call it with a argument of type int, which is built
in. int is not string.
So this will be replaced by "false", when the template is instantiated.
So this will call to_string().
Then we call the function with a string argument, so the parameter will be a string.
This will be replaced by "true" when the template is evaluated.
And this will return the argument directly.
So let's see if this works.
And no, it does not.
We get lots of compiler errors!
The reason is when we have an "if" statement, C++ says that all the branches must compile, even
if they are never taken.
And in this case, we have this compile-time expression. So the compiler will know that this branch
is never taken, if we have a string argument.
But it has to check it anyway.
And because there is no overload with a string argument, this does not compile.
However, if I have a C++17 compiler, and I change this to if constexpr.
Then this expression is evaluated at compile time.
So if we call it with a string argument, this will evaluate to true. And the compiler will only evaluate
this branch and include the result in the program.
It will completely ignore this,
and it will not even try to compile it.
So it does not matter what you put in here, when you are instantiating it for string.
And the same applies for int, in the opposite direction.
And there we are. So now, it compiles and runs.
So the problem with the run-time if is that all the branches must compile, even if they are not taken.
If we have a built-in parameter, then we cannot return a built-in from a function which is supposed
to return string.
And if we have a string argument, we cannot call to_string(), because that does not have an overload with a
string argument.
When we have constexpr if, only the branch which is taken needs to compile, and the compiler will
completely ignore
any other branches.
So, what did people do before C++17?
One way to do this, was with a template specialization.
So we have a generic template, which will take any parameter type and call to_string().
And then we have a specialization for string.
So this will "overrule" the generic template, if you like,
when we have a string. And just return it.
So here we have the two template functions.
We have the general one which will call to_string() and then the specialization for string, which
just returns it directly.
When we call this function with a built-in argument the compiler will instantiate this template,
because this one does not match at all.
When we call it with a string argument, the compiler will look at this one, which does match, but
then it will decide that the template specialization for string is a better match.
So it will instantiate the function which just returns the string directly. And that compiles and runs.
Now, this is real "black magic" stuff.
This is not recommended for viewers with a nervous disposition!
This is really well beyond the scope of this course.
So if none of this makes any sense, do not worry at all.
Just completely ignore it.
You will not be needed to do this again in this course.
So template specialization works,
if you have simple conditionals, but in the more general case, it does not really work.
This is a trick, which relies on something called "SFINAE". Which stands for "Substitution Failure Is Not An
"Error".
So when you have overloaded template functions, the compiler will only consider the ones which instantiate.
If one of these overloads cannot be instantiated, then the component will just ignore it.
So the idea is that you have one template function for each case, and then you make sure that the compiler
can only instantiate if that case is true.
And then, in the second bit of the twisted logic, there is this feature called "enable_if", which you
can use for switching instantiation on and off.
So all this means in effect is, "Compiler,
do not instantiate this if the parameter is a string".
And this is actually a simplified version using some syntax from C++17.
If you are doing this in C++11, it is even worse.
So as I say, just...
And then we have another overload, which is the inverse.
So the compiler will only instantiate this if the parameter is a string.
And then when we call this with an int parameter, the compiler will see that this one instantiates and
this one does not.
So it'll just ignore this template function and instantiate that one.
If we call this with a string argument, the compiler will see that this one cannot be instantiated.
So it will ignore this, and it will just instantiate this function.
So.
Right.
So, let's weigh up these different approaches. With a template specialization, you need to write several
template functions, which need to be in a specific order.
And this only really works if you have fairly simple logic. With this "SFINAE" and
"enable_if", you have complex and obscure code, which is difficult to get right, and hard to maintain. And finally,
constexpr
if. So you can just write a single function, which has normal looking code. Just like any other C++ function,
except it is evaluated at compile time.
Okay, so that is it for this video.
In the next video, we will have some examples of how to use
constexpr if.
But until then, keep coding!