WEBVTT

00:00.090 --> 00:04.320
Hello again! In this video, we are going to look at the constexpr if statement.

00:06.150 --> 00:08.550
This came in, in C++17.

00:09.180 --> 00:14.670
If you are reading discussions from that time, you may see it referred to as "if constexpr", or "static

00:14.680 --> 00:15.000
if".

00:15.990 --> 00:19.440
And this was one of the most requested changes to C++.

00:21.030 --> 00:24.300
And in this video I will try and explain why it was so requested.

00:25.470 --> 00:31.590
As the name suggests, constexpr if allows conditionals to be evaluated at compile time. As

00:31.590 --> 00:35.190
opposed to the plain "if" statements which is evaluated

00:35.520 --> 00:36.840
while the program is running.

00:40.500 --> 00:43.620
So this allows us to write expressions like this.

00:43.860 --> 00:49.560
if constexpr "a" less than "b". And, provided that "a" and "b" are constant expressions,

00:49.920 --> 00:52.020
this will be evaluated at compile time.

00:53.070 --> 00:59.550
So if "a" is less than "b", then the compiler will evaluate this code, and compile it into the program.

01:00.510 --> 01:06.180
And if "a" is not less than B said, the compiler will evaluate this code and compile it into the program.

01:07.080 --> 01:11.880
So the interesting point here is that only the branch which is taken gets included in the program.

01:12.270 --> 01:17.160
The other branches are ignored and do not appear in the program's source code. As seen by the compiler.

01:18.210 --> 01:23.130
So this means we can use constexpr if for doing conditional compilation.

01:25.250 --> 01:31.880
We were already able to do conditional compilation in C++, using the preprocessor directives. Which

01:31.880 --> 01:33.020
came from C.

01:33.980 --> 01:35.390
We have the directives

01:35.420 --> 01:36.830
"#if" and 

01:36.830 --> 01:42.650
"#ifdef". And these can be used for conditionally including or excluding code from the compilation.

01:43.460 --> 01:48.830
But there are a number of drawbacks to these, which we touched on in the overview of this section.

01:50.240 --> 01:56.960
These directives are processed by the preprocessor, which does not understand anything about C++.

01:57.470 --> 02:00.380
So it does not understand types or C++ syntax.

02:01.160 --> 02:04.070
It can only do simple text based substitution.

02:04.880 --> 02:06.710
It does not evaluate any arguments.

02:07.160 --> 02:14.120
So we can have problems involving operator precedence, and expressions with side effects. With constexpr

02:14.120 --> 02:19.970
if, this is executed by the compiler, during the compilation. So it has access to everything that

02:19.970 --> 02:25.730
the compiler knows about C++ and the program. In particular, it knows about type information.

02:27.800 --> 02:32.960
As an example, we are going to use a function which returns a string. It can obtain the string either

02:32.960 --> 02:38.390
from a library string, in which case it just returns the string directly. Or from a built-in type,

02:38.930 --> 02:44.930
in which case it calls the to_string() library function, which will return the string containing the same

02:44.930 --> 02:46.130
digits as the argument.

02:47.820 --> 02:53.100
We have to use a conditional because the to_string() function does not have an overload which takes a

02:53.100 --> 02:53.940
string argument.

02:54.240 --> 02:57.150
So if we try to pass a string to this, we would get a compiler error.

02:57.990 --> 03:01.440
So we need to know the type of the data that we are using for the string.

03:02.520 --> 03:05.130
So we would have some pseudocode, which looks like this.

03:05.490 --> 03:08.700
If the argument is a string, then return it directly.

03:09.300 --> 03:13.440
Otherwise, call to_string(), and then return whatever that gives us.

03:15.000 --> 03:19.530
And then we are going to write this as a template function, which checks the argument's type traits.

03:19.920 --> 03:22.260
And if you are not too sure about that, or the syntax, do not worry.

03:22.380 --> 03:24.030
This is just an example.

03:26.990 --> 03:32.300
So we could have a function which looks like this with the usual C++ if statements.

03:32.930 --> 03:35.330
Then we have this bit of template

03:35.330 --> 03:36.230
metaprogramming code.

03:37.010 --> 03:37.840
Do not worry about this.

03:37.850 --> 03:42.110
It just means that, if the parameter is a string, then this will be replaced by "true".

03:42.560 --> 03:48.260
And if the parameter is not a string, then this will be replaced by "false", when the template is instantiated.

03:51.600 --> 03:52.620
So let's try this out.

03:52.630 --> 03:56.370
So here is our template function with the if statement.

03:57.960 --> 04:02.910
We have some code which calls it. First of all, we call it with a argument of type int, which is built

04:02.910 --> 04:05.250
in. int is not string.

04:05.340 --> 04:08.670
So this will be replaced by "false", when the template is instantiated.

04:09.450 --> 04:11.130
So this will call to_string().

04:12.920 --> 04:17.330
Then we call the function with a string argument, so the parameter will be a string.

04:17.750 --> 04:20.840
This will be replaced by "true" when the template is evaluated.

04:21.650 --> 04:23.840
And this will return the argument directly.

04:24.560 --> 04:25.670
So let's see if this works.

04:27.820 --> 04:28.670
And no, it does not.

04:28.720 --> 04:30.220
We get lots of compiler errors!

04:31.840 --> 04:37.540
The reason is when we have an "if" statement, C++ says that all the branches must compile, even

04:37.540 --> 04:38.440
if they are never taken.

04:39.640 --> 04:45.610
And in this case, we have this compile-time expression. So the compiler will know that this branch

04:45.610 --> 04:47.410
is never taken, if we have a string argument.

04:47.920 --> 04:49.210
But it has to check it anyway.

04:49.630 --> 04:53.740
And because there is no overload with a string argument, this does not compile.

04:55.960 --> 05:01.240
However, if I have a C++17 compiler, and I change this to if constexpr.

05:02.730 --> 05:05.340
Then this expression is evaluated at compile time.

05:06.300 --> 05:12.930
So if we call it with a string argument, this will evaluate to true. And the compiler will only evaluate

05:13.380 --> 05:17.280
this branch and include the result in the program.

05:17.460 --> 05:18.780
It will completely ignore this,

05:18.840 --> 05:20.250
and it will not even try to compile it.

05:20.610 --> 05:24.390
So it does not matter what you put in here, when you are instantiating it for string.

05:25.080 --> 05:27.600
And the same applies for int, in the opposite direction.

05:30.670 --> 05:32.860
And there we are. So now, it compiles and runs.

05:35.790 --> 05:40.620
So the problem with the run-time if is that all the branches must compile, even if they are not taken.

05:41.460 --> 05:47.060
If we have a built-in parameter, then we cannot return a built-in from a function which is supposed

05:47.160 --> 05:47.970
to return string.

05:48.690 --> 05:53.580
And if we have a string argument, we cannot call to_string(), because that does not have an overload with a

05:53.580 --> 05:54.390
string argument.

05:56.400 --> 06:01.950
When we have constexpr if, only the branch which is taken needs to compile, and the compiler will

06:01.950 --> 06:02.910
completely ignore

06:03.150 --> 06:04.170
any other branches.

06:07.700 --> 06:10.370
So, what did people do before C++17?

06:10.940 --> 06:13.730
One way to do this, was with a template specialization.

06:14.330 --> 06:20.540
So we have a generic template, which will take any parameter type and call to_string().

06:21.440 --> 06:23.990
And then we have a specialization for string.

06:24.260 --> 06:28.400
So this will "overrule" the generic template, if you like,

06:28.610 --> 06:30.530
when we have a string. And just return it.

06:31.580 --> 06:33.650
So here we have the two template functions.

06:33.650 --> 06:39.290
We have the general one which will call to_string() and then the specialization for string, which

06:39.290 --> 06:40.670
just returns it directly.

06:41.990 --> 06:47.240
When we call this function with a built-in argument the compiler will instantiate this template,

06:47.240 --> 06:49.270
because this one does not match at all.

06:50.330 --> 06:54.860
When we call it with a string argument, the compiler will look at this one, which does match, but

06:54.860 --> 06:59.060
then it will decide that the template specialization for string is a better match.

06:59.510 --> 07:06.560
So it will instantiate the function which just returns the string directly. And that compiles and runs.

07:08.420 --> 07:11.420
Now, this is real "black magic" stuff.

07:12.080 --> 07:14.960
This is not recommended for viewers with a nervous disposition!

07:15.680 --> 07:18.090
This is really well beyond the scope of this course.

07:18.110 --> 07:20.780
So if none of this makes any sense, do not worry at all.

07:20.900 --> 07:22.070
Just completely ignore it.

07:22.490 --> 07:24.770
You will not be needed to do this again in this course.

07:26.630 --> 07:28.370
So template specialization works,

07:28.460 --> 07:32.870
if you have simple conditionals, but in the more general case, it does not really work.

07:34.370 --> 07:40.280
This is a trick, which relies on something called "SFINAE". Which stands for "Substitution Failure Is Not An

07:40.280 --> 07:40.610
"Error".

07:41.180 --> 07:47.510
So when you have overloaded template functions, the compiler will only consider the ones which instantiate.

07:48.140 --> 07:52.460
If one of these overloads cannot be instantiated, then the component will just ignore it.

07:54.180 --> 07:59.730
So the idea is that you have one template function for each case, and then you make sure that the compiler

07:59.730 --> 08:01.860
can only instantiate if that case is true.

08:03.030 --> 08:09.540
And then, in the second bit of the twisted logic, there is this feature called "enable_if", which you

08:09.540 --> 08:11.880
can use for switching instantiation on and off.

08:13.170 --> 08:16.320
So all this means in effect is, "Compiler,

08:16.560 --> 08:19.860
do not instantiate this if the parameter is a string".

08:20.640 --> 08:24.750
And this is actually a simplified version using some syntax from C++17.

08:24.960 --> 08:27.810
If you are doing this in C++11, it is even worse.

08:29.100 --> 08:31.040
So as I say, just...

08:34.010 --> 08:36.620
And then we have another overload, which is the inverse.

08:36.650 --> 08:41.030
So the compiler will only instantiate this if the parameter is a string.

08:42.560 --> 08:48.980
And then when we call this with an int parameter, the compiler will see that this one instantiates and

08:48.980 --> 08:49.910
this one does not.

08:50.300 --> 08:53.780
So it'll just ignore this template function and instantiate that one.

08:55.040 --> 08:59.330
If we call this with a string argument, the compiler will see that this one cannot be instantiated.

08:59.360 --> 09:03.020
So it will ignore this, and it will just instantiate this function.

09:07.440 --> 09:07.890
So.

09:10.040 --> 09:10.160
Right.

09:10.250 --> 09:16.910
So, let's weigh up these different approaches. With a template specialization, you need to write several

09:17.180 --> 09:20.390
template functions, which need to be in a specific order.

09:21.110 --> 09:26.960
And this only really works if you have fairly simple logic. With this "SFINAE" and

09:26.960 --> 09:33.500
"enable_if", you have complex and obscure code, which is difficult to get right, and hard to maintain. And finally,

09:33.500 --> 09:33.950
constexpr

09:34.100 --> 09:40.460
if. So you can just write a single function, which has normal looking code. Just like any other C++ function,

09:40.790 --> 09:42.890
except it is evaluated at compile time.

09:44.120 --> 09:45.920
Okay, so that is it for this video.

09:46.100 --> 09:49.220
In the next video, we will have some examples of how to use

09:49.220 --> 09:49.610
constexpr if.

09:50.210 --> 09:52.390
But until then, keep coding!
