28

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.

αλεχολυτ
  • 4,792
  • 1
  • 35
  • 71
NYM
  • 309
  • 3
  • 5
  • g++ 7.3 and ubuntu 16.04 – NYM Apr 26 '18 at 20:47
  • 4
    Compiles with GCC 7.1, 7.2, 7.3; fails with GCC 6.3 online link: https://godbolt.org/g/oRDiXQ – Richard Critten Apr 26 '18 at 20:49
  • 1
    g++ -std=c++17 main.cpp -o test main.cpp: In function ‘int main()’: main.cpp:8:32: error: invalid type argument of unary ‘*’ (have ‘int’) std::cout << "Ptr to " << *value << std::endl; // Error – NYM Apr 26 '18 at 20:49
  • NYM, I share your surprise... but - to simplify the question, I suggest you use [this simpler example](https://godbolt.org/g/c5Daj3) which doesn't involve any printing. – einpoklum Apr 26 '18 at 21:23
  • [cppreference](https://en.cppreference.com/w/cpp/language/if) explains it rather well. – n. m. could be an AI Oct 29 '18 at 14:57

5 Answers5

29

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.

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 ints, 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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 4
    I misread your answer initially, so deleted my earlier comment, but in a template instantiation where the `if constexpr` condition becomes constant, the branch not taken *will* be parsed, but *won't* have semantic analysis performed on it. That's the whole point, and that's why the OP's `print` works even though it contains `*value` where `value` is an `int`. It's the semantic analysis that would flag it as an error. –  Apr 26 '18 at 21:08
  • 4
    @hvd It will have *some* analysis performed on it. If you do `template void foo(int i) { if constexpr(some_condition_v) { std::cout << *i; } }`, that could still be diagnosed because the expression isn't dependent (the code is ill-formed, but no diagnostic is required). – Barry Apr 26 '18 at 21:12
  • @Barry: I thought type mismatches like this one are one of the poster-child use cases for if constexpr... – einpoklum Apr 26 '18 at 21:17
  • 2
    @einpoklum Trying to just dereference an `int`? No. Trying to dereference a `T`? Yes. – Barry Apr 26 '18 at 21:19
  • @Barry Good point, I should've mentioned that too. It's exactly as it is with templates outside of `if constexpr`: there must be *some* instantiation where the code is valid, otherwise as you said, the program is ill-formed, no diagnostic required. –  Apr 26 '18 at 21:21
  • @Barry: But you're derefering a variable. I would think that should be good enough... – einpoklum Apr 26 '18 at 21:24
  • @hvd: Isn't it Turing-complete to check "is there some instantiation which"? – einpoklum Apr 26 '18 at 21:24
  • @einpoklum Right, and that's why no diagnostic is required. Compilers are free to check what they can/want to to provide a better service to their users, but it's up to the implementation how much trouble they want to go through. But for non-dependent expressions, proving that no instantiation can make the expression valid is trivial. –  Apr 26 '18 at 21:30
  • 1
    @einpoklum I believe you meant either NP-complete or undecidable, not Turing-complete :) – lisyarus Apr 26 '18 at 22:30
  • @lisyarus: [Turing completeness](https://en.wikipedia.org/wiki/Turing_completeness) implies undecidability. – einpoklum Apr 26 '18 at 22:58
  • +1 for `You simply can't use if constexpr to avoid compiling non-template code.` Too bad that's not how `if constexpr` works, though, it would have removed the need for endless `#ifdef`s. – cantordust Apr 27 '18 at 04:14
  • 3
    @einpoklum No, it doesn't in a strict sense: Turing completeness is a property of computational abstractions (e.g. programming languages), while undecidability is a property of concrete computational problems. – lisyarus Apr 27 '18 at 05:58
  • You did not say that the initialy if constexpr should have behaved in non template as in template function... and compiler implementer complained about a 100h of works to implement it, but for coder this is (10h of work * 1 000 000 coder) of work! The C++commitee does not work anymore for coder but compiler implementer, this is a very bad. – Oliv Apr 27 '18 at 13:41
  • @Oliv If someone puts together a compelling need for such a use-case, it can happen. Whinging about how the committee does not care about coders is not a productive use of time and just reflects very poorly on you. – Barry Apr 27 '18 at 13:50
  • @Barry But the need disapear because as C++ coder we are so fast to find work around. But still we have to think of it, and all these inconsistencies, smal exceptions to rules, all around in the language, while they do not make sense nor for the coder neither for the machine, shall be removed from the language. Whatsoever I think it is too late now for C++, this self destructing behavior has been institutionnalized. – Oliv Apr 28 '18 at 19:43
12

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.

lisyarus
  • 15,025
  • 3
  • 43
  • 68
10

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.

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • Good explanation. You said *Even better, the compiler won't instantiate templates that are in a discarded branch of a if constexpr*. That's OK because your condition is just `false`. But would happen if the condition is value-dependant (depends on template parameter?) and the condition gets evaluated to false, Does the discarded statement is not instantiated also? – mada Jan 18 '22 at 14:50
  • @AccessDenied [Yes](https://godbolt.org/z/8sqPz35rz). The `if constexpr` don't get special treatment whether it's in a template or not, that the whole point of my answer :) – Guillaume Racicot Jan 18 '22 at 15:04
  • but the standard says not: *During the instantiation of an enclosing templated entity, if the condition is **NOT** value-dependent after its instantiation, the discarded substatement (if any) is not instantiated.* [check this][1]. I expect that `T::notexsit` to be instantiated and get a compile error. But that's not happening though the condition is value-dependent . [1]: https://godbolt.org/z/E1MefnG8v – mada Jan 18 '22 at 15:20
  • Maybe I not fully understand, what does the **value-dependent** mean in this context – mada Jan 18 '22 at 15:26
  • 1
    @AccessDenied I think, to my understanding of the standard, it means that if there an enclosing template entity (any level of template entity) to the `if constexpr` and the condition of that `if constexpr` is not a dependent expression anymore (since the template is resolved), any discarded branches are not instantiated. Since they are not instantiated, second phase name lookup don't run and it compiles successfully. – Guillaume Racicot Jan 18 '22 at 15:44
  • I agree with your thinking since the standard said *if the condition is not value-dependent after its instantiation*. But I think there is no possibility of that happening. After the template has been instantiated why the condition is still value-dependent. I know that, as long as the condition finally gets evaluated to false, the containing statements get discarded; but I am trying to understand what's the standard intend by this sentence *if the condition is **not value-dependent after its instantiation**, the discarded substatement (if any) is not instantiated* – mada Jan 18 '22 at 15:57
3

Outside a template, a discarded statement is fully checked. if constexpr is not a substitute for the #if preprocessing directive.

here image

1

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

Martin Morterol
  • 2,560
  • 1
  • 10
  • 15