I would like to know why if constexpr
works only in template functions, even if the type is deduced by the decltype from the input parameter.
The thing is, it also work in non template, just not in the way you would expect.
For if constexpr
to work like you stated, you not only need a template, but you need the contained expressions to be dependent on the template parameters.
Let's go step by step why this is made that way in C++, and what are the implications.
Let's start simple. Does the following code compile?
void func_a() {
nonexistant();
}
I think we will all agree that it won't compile, we are trying to use a function that hasn't been declared.
Let's add one layer.
Does the following code compile?
template<typename T>
void func_b_1() {
nonexistant();
}
With a correct compiler, this will not compile.
But why is that? You could argue that this code is never actually compiled, since the template is never instantiated.
The standard define something they call two phase name lookup. This is that even if the template is not instantiated, the compiler must perform name lookup an anything that don't depend on the template parameter.
And that make sense. If the expression nonexistant()
don't depend on T
, why would its meaning change with T
? Hence, this expression is the same as in func_a
in the eye of the compiler.
So how about dependent names?
template<typename T>
void func_b_2() {
T::nonexistant();
}
This will compile! Why is that? Nowhere in this code there's a function called nonexistant
. Yet, you feed that into a compiler as the whole codebase and it will gladly accept it.
And the standard even says that it has to accept it. This is since there could be a T
containing nonexistant
somewhere. So if you instantiate the template with a type that has the static member function nonexistant
it will compile and call the function. If you instantiate the template with a type that don't have the function, it won't compile.
As you can see, the name lookup is done during instantiation. This is called second phase name lookup. The second phase name lookup is done only during instantiation.
Now, enter if constexpr
.
To make such construct working well with the rest of the language properly, it has been decided that if constexpr
is defined as a branch for instantiation. As such, we can make some code non-instantiated, even in non templates!
extern int a;
void helper_1(int*);
void func_c() {
if constexpr (false) {
helper_1(&a);
}
}
The answer is that helper_1
and a
are not ODR used. We could leave helper_1
and a
not defined and there would not be linker errors.
Even better, the compiler won't instantiate templates that are in a discarded branch of a if constexpr
:
template<typename T>
void helper_2() {
T::nonexistant();
}
void func_d() {
if constexpr (false) {
helper_2<int>();
}
}
This code won't compile with a normal if
.
As you can see, the discarded branch of a if constexpr
work just like a template that hasn't been instantiated, even in non template code.
Now let's mix it up:
template<typename T>
void func_b_3() {
if constexpr (false) {
nonexistant();
}
}
This is just like our template function in the beginning. We said that even if the template was not instantiated, the code was invalid, since the invalid expression don't depend on T
. We also said that if constexpr
is a branch in the instantiation process. The error happen before instantiation. This code won't compile either.
So finally, this code won't compile either:
void func_e() {
if constexpr (false) {
nonexistant();
}
}
Even though the content of the if constexpr
is not instantiated, the error happen because the fist name lookup step is done, and the error happen before the instantiation process. It is just that in this case, there is no instantiation, but it doesn't matter at this point.
So what are the uses of if constexpr
? Why does it seem to work only in templates?
The thing is, it doesn't work differently in templates. Just as we saw with func_b_3
, the error still happen.
But, this case will work:
template<typename T>
void helper_3() {
if constexpr (false) {
T::nonexistant();
}
}
void func_f() {
helper_3<int>();
}
The expression int::nonexistant()
is invalid, but the code compile. This is because since T::nonexistant()
is an expression that depends on T
, name lookup is done in the second phase. The second phase of name lookup is done during template instantiation. The if constexpr
branch that contain T::nonexistant()
is always the discarded part so the second phase of name lookup is never done.
There you go. if constexpr
is not about not compiling a portion of code. Just like template, they are compiled and any expression that name lookup can be done is done. if constexpr
is about controlling instantiation, even in non template function. All rules that applies to templates also applies to all branch of the if constexpr
. Two phase name lookup still applies and allow programmers to not instantiate some part of the code that would otherwise not compile if instantiated.
So if a code cannot compiled in a template that is not instantiated, it won't compile in the branch of the if constexpr
that is not instantiated.