Reputation: 349
I tried to play with the C++17 standard. I tried to use one of the features of C++17 if constexpr
. And I had a problem... Please take a look at the following code. This compiles without errors. In the following code, I tried to use if constexpr
to check if it is a pointer.
#include <iostream>
#include <type_traits>
template <typename T>
void print(T value)
{
if constexpr (std::is_pointer_v<decltype(value)>)
std::cout << "Ptr to " << *value << std::endl; // Ok
else
std::cout << "Ref to " << value << std::endl;
}
int main()
{
auto n = 1000;
print(n);
print(&n);
}
But when I rewrite the above code, as shown below, where if constexpr
is in the main
function:
#include <iostream>
#include <type_traits>
int main()
{
auto value = 100;
if constexpr (std::is_pointer_v<decltype(value)>)
std::cout << "Ptr to " << *value << std::endl; // Error
else
std::cout << "Ref to " << value << std::endl;
}
I get a compilation error:
main.cpp:8:32: error: invalid type argument of unary ‘*’ (have ‘int’) std::cout << "Ptr to " << *value << std::endl;
Problem is not in the main function. This can be any function similar to the following.
void print()
{
auto value = 100;
if constexpr (std::is_pointer_v<decltype(value)>)
std::cout << "Ptr to " << *value << std::endl; // Error
else
std::cout << "Ref to " << value << std::endl;
}
int main()
{
print();
}
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.
Upvotes: 32
Views: 8071
Reputation: 3843
The answer by @GuillaumeRacicot is fantastic (the examples are really useful) but still a little confusing because, like the other answers here, it mixes together different types of tricks to (in a vague sense) "avoid compiling code."
This all becomes easier to understand if we split into three specific types of tricks that different people might want to do. Some tricks can be done entirely with if constexpr
(without any need for templates or dependent expressions), and other tricks need if constexpr
, templates, and dependent expressions at the call/reference site.
Say you have:
and your goal is to be able to conditionally "disable" the reference in the sense that you don't need to ever define that object/function.
In other words, your goal is to avoid linker errors about a missing object/function. Your object/function remains ODR-used technically, but you want to somehow not have to define it.
You can do this trick using only if constexpr
, as seen in the example from @GuillaumeRacicot and as guaranteed by the C++ spec in stmt.if.2 note 1 and example 2:
extern int a; // never need to define this
void helper_1(int*); // never need to define this
void func_c()
{
if constexpr (false)
{
helper_1(&a); // won't cause any linker errors
}
}
You don't need to use any templates or dependent expressions.
Say you have:
and your goal is to conditionally "disable" the reference in the sense that there won't be compile errors inside the template.
In other words, you want to prevent the template from being instantiated with particular actual template arguments, even though you have code which does exactly that.
This too can be done using only if constexpr
, as seen in this great code from @GuillaumeRacicot and as guaranteed by the C++ spec in stmt.if.2 example 2:
template<typename T>
void helper_2()
{
// this code will not compile unless T has a "nonexistent" member
T::nonexistent();
}
void func_d()
{
if constexpr (false)
{
// will not generate error about "int::nonexistent"
helper_2<int>();
}
}
The false branch of if constexpr
prevents any template instantiations inside the branch from happening.
You don't need to use any templates or dependent expressions at the call/reference site (in this example, func_d()
).
Ok, now we come to the trick that most people probably had in mind.
Say you want to call a function func
that
Example:
void func(myclass m, float f)
{
}
template< class X >
void danger(X &x)
{
if constexpr (false)
{
// NO compile error generated here
// this function does not exist
nonexistent(x);
// this function exists, but with different parameters
func(x);
};
}
Obviously, in a real-world situation, the if constexpr
condition wouldn't just be false
, but rather some consteval
expression that guards against the cases that would otherwise make compiler errors.
To accomplish this trick, you need all of these necessary conditions:
if constexpr
that is also inside the template
if constexpr
inside templatesif constexpr
condition definitely can use the template parameters; the only time it wouldn't be value-dependent "after instantiation" is an incredibly rare corner case of nested templates that you probably won't hit (for an example, see this page where it says "The condition remains value-dependent after instantiation is a nested template [sic]" and read the comment in the sample code about implicit lambda captures).if constexpr
(in other words, inside a branch that is not taken)This trick works because code inside the discarded branch of the if constexpr
is not instantiated. And lookup of dependent names happens during instantiation.
If you tried to call a function using a non-dependent expression, e.g. other_nonexistent()
, the compiler will give you an error regardless of any if constexpr
and regardless of any template trickery. The compiler checks non-dependent names the instant it sees them, before any if constexpr
branches are taken and before any templates are instantiated.
Upvotes: 1
Reputation: 41840
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 stated 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.
Upvotes: 18
Reputation: 170
Outside a template, a discarded statement is fully checked. if constexpr
is not a substitute for the #if
preprocessing directive.
Upvotes: 3
Reputation: 2900
I am a little late to the party but a nice trick I use when I need if constexpr
in a non template function is to wrap my code in a lambda.
struct Empty
{
};
void foo()
{
/* foo is not template, will not compile
if constexpr ( false )
{
std::cout << Empty{}.bar; // does not exist
}
*/
// now the code is in a lambda with auto param, so basicly a template method
[&](const auto& empty)
{
if constexpr ( false )
{
std::cout << empty.bar;
}
}(Empty{});
}
demo : https://wandbox.org/permlink/XEgZ6PXPLyyjXDlO
The fact I can use the [&]
syntax make this solution way simpler to use than create an helper method since I don't need to forward all the parameters I need inside my if constexpr
Upvotes: 2
Reputation: 304082
I would like to know why "
if constexpr
" works only in template functions, even if the type is deduced by thedecltype
from the input parameter.
This is by design.
if constexpr
will not instantiate the branch not taken if it's within a template. It won't just treat the branch not taken as token soup and avoid parsing it or performing semantic analysis entirely. Both sides are still going to be analyzed, and since *value
is ill-formed for int
s, that's an error.
You simply can't use if constexpr
to avoid compiling non-template code. It's only to avoid instantiating template code that's potentially invalid-for-the-particular-specialization.
Upvotes: 32
Reputation: 15587
C++ standard, clause 9.4.1:
If the if statement is of the form if constexpr, the value of the condition shall be a contextually converted constant expression of type bool (8.6); this form is called a constexpr if statement. If the value of the converted condition is false, the first substatement is a discarded statement, otherwise the second substatement, if present, is a discarded statement. During the instantiation of an enclosing templated entity (Clause 17), if the condition is not value-dependent after its instantiation, the discarded substatement (if any) is not instantiated.
(emphasis mine)
So, a substatement of a constexpr if
still gets instantiated if it is not inside a template, thus it must at least compile.
Upvotes: 12