If Constexpr - Why Is Discarded Statement Fully Checked

if constexpr - why is discarded statement fully checked?

This is just how constexpr if works. If we check [stmt.if]/2

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; 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 ([temp.pre]), if the condition is not value-dependent after its instantiation, the discarded substatement (if any) is not instantiated.[...]

emphasis mine

So we can see that we only do not evaluate the discarded expression if we are in a template and if the condition is value-dependent. main is not a function template so the body of the if statement is still checked by the compiler for correctness.

Cppreference also says this in their section about constexpr if with:

If a constexpr if statement appears inside a templated entity, and if condition is not value-dependent after instantiation, the discarded statement is not instantiated when the enclosing template is instantiated .

template<typename T, typename ... Rest>
void g(T&& p, Rest&& ...rs) {
// ... handle p
if constexpr (sizeof...(rs) > 0)
g(rs...); // never instantiated with an empty argument list.
}

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

void f() {
if constexpr(false) {
int i = 0;
int *p = i; // Error even though in discarded statement
}
}

Behavior of If constexpr in C++

Consider this example:

#include <iostream>
#include <string>

template <typename T>
void foo() {
T t;
if constexpr (std::is_same_v<T,std::string>){
std::cout << t.find("asd");
} else {
t = 0;
std::cout << t;
}
}

int main () {
foo<int>(); // (2)
}

When T is a type that does not have a find method, then std::cout << t.find("asd") is an error. Nevertheless, the template is ok.


  1. What 'instantiaded' mean ?

The template is instantiated in (2). foo is just a template, instantiating it results in a function foo<int> that you can call.

  1. What does 'discarded' mean in the text below?

The true-branch is discarded when foo<int> is instantiated (because the condition is false). Hence, even though int has no find method, the code compiles without error.

Now consider this similar, but very different example:

#include <iostream>

int main () {
int x = 0;
if constexpr (true) {
std::cout << x;
} else {
x.find("asd");
}
}

  1. What 'checked' mean?

The text is a little contrived, it says that in the above example the false branch is discarded, but nevertheless it is checked, because it is outside of a template. It is just the english term: "checked" means the compiler checks if the code is correct. int has no method find, hence the above results in an error:

<source>:8:15: error: request for member 'find' in 'x', which is of non-class type 'int'
8 | x.find("asd");
| ^~~~

Even though this statement is never executed, it must be valid code.

Why does this usage of C++17 if constexpr fail?

This is not possible outside the template!

From cppreference.com

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

void f() {
if constexpr(false) {
int i = 0;
int *p = i; // Error even though in discarded statement
}
}


Any idea how to skip compilation of X2?


One option is to provide a template function for that.
template<typename T>
void test()
{
if constexpr (std::is_null_pointer_v<T>)
X2;
else
X1;
}

int main()
{
std::map<std::string, int> map;
test<decltype(map)>(); // now chooses the X1
}

Thanks to @HolyBlackCat and @MSalters. As they pointed out, the above solution is ill-formed NDR (therefore, compiling with MSVC compiler does not make any sense and on the other hand
the GCC and clang at least catch this by providing some compiler errors )
which has been detailed in the @HolyBlackCat's,
answer!

Therefore can we skip the compilation of X2?

Unfortunately, NO as per your code!!
The preprocessor will be executed before the compilation of the translation unit.
Therefore one can not provide the type information (i.e. decltype(map)) to #if directives.
Hence for your case, there is no other way.

Good lessons from this post:

  • Your program is, however, is a good example to avoid such kind macro
    and constexpr if mixing.
  • Secondly, check the correctness of the code by different
    compilers if possible!

I would suggest having a function overload for PP (and of course there are many other ways) to your case, by which you could get a well-formed code:

See a demo.

#include <string>
#include <iostream>
#include <type_traits>
#include <map>

void pp(const std::string& str)
{
std::cout << str << std::endl;
}

template<typename... T> void pp(const T&... args)
{
// do something with args!
}

template<typename T>
constexpr void test()
{
if constexpr (std::is_null_pointer_v<T>)
pp("x", "x"); // call with args
else
pp("x"); // call with string
}

#if Vs if constexpr

Note that, if if constexpr is not part of a template, then the other parts of the if (such as the elses) are still compiled and checked for validity.

From cppreference:

If a constexpr if statement appears inside a templated entity, and if condition is not value-dependent after instantiation, the discarded statement is not instantiated when the enclosing template is instantiated .

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

Is it possible to remove control flow statements with if constexpr?

if constexpr is not a macro. Indeed, it only gains powers distinct from a regular if statement in the specific circumstances of both being in a template and the condition itself being based on template parameters. And even then, the discarded statements have to be at least potentially legitimate.

Is if constexpr useful outside of templates?

Is it true, that if expr is not dependent on a template parameter, then no branches of if constexpr(expr) will be discarded? If yes, where does the standard say so? […]

Yes, that is true. You're looking for [stmt.if]/2. Specifically this part:

[…] 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. […]

The best example I could find for a case where you would end up being value-dependent after instantiation is the one given by cppreference.com:

template<class T> void g() {
auto lm = [](auto p) {
if constexpr (sizeof(T) == 1 && sizeof p == 1) {
// this condition remains value-dependent after instantiation of g<T>
}
};
}

Is if constexpr useful outside of templates? If yes, can you give some examples to understand its usefulness?

While all branches will be instantiated when the if constexpr does not appear inside of a template, [basic.def.odr]/10 still applies:

Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program outside of a discarded statement; […]

emphasis mine. That effectively means that an odr-use of an entity in a discarded statement doesn't count. For example:

void blub();

constexpr bool use_blub = false;

void f()
{
if constexpr (use_blub)
{
blub();
}
}

The call to blub() will not require that your program have a definition of blub() if the condition is false. Using a normal if, the program would still be required to provide a definition of blub() somewhere, even if it is never used. So you could, e.g., use if constexpr to toggle between calling some library function and calling some fallback implementation depending on whether the library is available (and being linked to). Apart from that, hypothetically, a compiler might not warn about unreachable code if it is unreachable due to an if constexpr like it potentially would with a normal if. I couldn't come up with an example of this using any actual compiler, however…

Difference between if constexpr() Vs if()

The only difference is that if constexpr is evaluated at compile time, whereas if is not. This means that branches can be rejected at compile time, and thus will never get compiled.


Imagine you have a function, length, that returns the length of a number, or the length of a type that has a .length() function. You can't do it in one function, the compiler will complain:

template<typename T>
auto length(const T& value) noexcept {
if (std::integral<T>::value) { // is number
return value;
else
return value.length();
}

int main() noexcept {
int a = 5;
std::string b = "foo";

std::cout << length(a) << ' ' << length(b) << '\n'; // doesn't compile
}

Error message:

main.cpp: In instantiation of 'auto length(const T&) [with T = int]':
main.cpp:16:26: required from here
main.cpp:9:16: error: request for member 'length' in 'val', which is of non-class type 'const int'
return val.length();
~~~~^~~~~~

That's because when the compiler instantiates length, the function will look like this:

auto length(const int& value) noexcept {
if (std::is_integral<int>::value) { // is number
return value;
else
return value.length();
}

value is an int, and as such doesn't have a length member function, and so the compiler complains. The compiler can't see that statement will never be reached for an int, but it doesn't matter, as the compiler can't guarantee that.

Now you can either specialize length, but for a lot of types (like in this case - every number and class with a length member function), this results in a lot of duplicated code. SFINAE is also a solution, but it requires multiple function definitions, which makes the code a lot longer than it needs to be compared to the below.

Using if constexpr instead of if means that the branch (std::is_integral<T>::value) will get evaluated at compile time, and if it is true then every other branch (else if and else) gets discarded. If it is false, the next branch is checked (here else), and if it is true, discard every other branch, and so on...

template<typename T>
auto length(const T& value) noexcept {
if constexpr (std::integral<T>::value) { // is number
return value;
else
return value.length();
}

Now, when the compiler will instantiate length, it will look like this:

int length(const int& value) noexcept {
//if (std::is_integral<int>::value) { this branch is taken
return value;
//else discarded
// return value.length(); discarded
}

std::size_t length(const std::string& value) noexcept {
//if (std::is_integral<int>::value) { discarded
// return value; discarded
//else this branch is taken
return value.length();
}

And so those 2 overloads are valid, and the code will compile successfully.



Related Topics



Leave a reply



Submit