Why does if constexpr require an else to work?
Asked Answered
E

2

14

I am trying to use if constexpr in the following way:

template<template <typename First, typename Second> class Trait,
    typename First, typename Second, typename... Rest>
constexpr bool binaryTraitAre_impl()
{
    if constexpr (sizeof... (Rest) == 0)
    {
        return Trait<First, Second>{}();    
    }
    return Trait<First, Second>{}() and binaryTraitAre_impl<Trait, Rest...>();
}

Example use case:

static_assert(binaryTraitAre_impl<std::is_convertible,
    int, int&,
    int*, void*>());

But this fails to compile

clang:

error: no matching function for call to 'binaryTraitAre_impl'
        return Trait<First, Second>{}() and binaryTraitAre_impl<Trait, Rest...>();
                                            ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

gcc:

prog.cc: In instantiation of 'constexpr bool binaryTraitAre_impl() [with Trait = std::is_convertible; First = int*; Second = void*; Rest = {}]':
prog.cc:9:80:   required from 'constexpr bool binaryTraitAre_impl() [with Trait = std::is_convertible; First = int; Second = int&; Rest = {int*, void*}]'
prog.cc:15:83:   required from here
prog.cc:9:80: error: no matching function for call to 'binaryTraitAre_impl<template<class _From, class _To> struct std::is_convertible>()'
    9 |         return Trait<First, Second>{}() and binaryTraitAre_impl<Trait, Rest...>();
      |                                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~
prog.cc:3:17: note: candidate: 'template<template<class First, class Second> class Trait, class First, class Second, class ... Rest> constexpr bool binaryTraitAre_impl()'
    3 |  constexpr bool binaryTraitAre_impl()
      |                 ^~~~~~~~~~~~~~~~~~~
prog.cc:3:17: note:   template argument deduction/substitution failed:
prog.cc:9:80: note:   couldn't deduce template parameter 'First'
    9 |         return Trait<First, Second>{}() and binaryTraitAre_impl<Trait, Rest...>();
      |                                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~

But I found the error goes away once I add else:

template<template <typename First, typename Second> class Trait,
    typename First, typename Second, typename... Rest>
constexpr bool binaryTraitAre_impl()
{
    if constexpr (sizeof... (Rest) == 0)
    {
        return Trait<First, Second>{}();
    }
    else
    {
        return Trait<First, Second>{}() and binaryTraitAre_impl<Trait, Rest...>();
    }
}

live demo

What happened? Why can the compiler not infer the else in this case?

Enlargement answered 17/12, 2018 at 15:50 Comment(8)
Is there a dupe for those kinds of questions? It seems like if constexpr is a counter-intuitive feature that a lot of people get wrong. I'm pretty sure I have already seen a question like that...Molder
@Rakete could not find one in the if-constexpr tag page (OK, I only looked in the first page). If you find one, I'll delete my answer.Wilterdink
@MatthieuBrucher same though.Molder
If it has a official idiom, it'll be more SEO friendlyEnlargement
@Molder Yeah, I feel like I've seen a question like this before - but I always struggle to find good dupes (which is why I end up resorting to favoriting common dupes).Baranowski
Note that your implementation is incorrect - it would give me true for binaryTraitAre_impl<is_same, int, double>() (this is incidental to the question as a whole)Baranowski
@Baranowski thanks! I'll add more tests cases locally later.Enlargement
@Baranowski I'll probably store this one as my reference for such duplicates now.Wilterdink
W
18

This is the excerpt from cppreference on constexpr if:

Constexpr If The statement that begins with if constexpr is known as the constexpr if statement.

In a constexpr if statement, the value of condition must be a contextually converted constant expression of type bool. If the value is true, then statement-false is discarded (if present), otherwise, statement-true is discarded.

It is clear that only one of the two branches is discarded. In your case, the culprit code cannot be discarded because it's outside the else clause.

Wilterdink answered 17/12, 2018 at 15:54 Comment(0)
L
14

if constexpr when the clause is true doesn't eliminate code outside of the corresponding else block.

You could extend C++ to do that, but it quickly becomes a pain. Only the most trivial cases are obvious, and specifying what the trivial cases are is a pain. I mean do you cover:

if constexpr( blah ){
  if (true) return 7;
}

? How about

if constexpr( blah ){
  if (blah) return 7;
  else exit(-1);
}

? Or

if constexpr( blah ){
  if (blah) return 7;
  else return false;
}

or

if constexpr( blah ){
  if (blah) goto foo;
  return false;
foo: return true;
}

or how about:

if constexpr( blah ){
  std::size_t count = 0;
  while (foo != 1 && (++count < (std::size_t)-1))
    switch (foo%2) {
      case 1: foo = 3*foo+1;
      case 0: foo = foo/2;
    }
  }
  if (count < (std::size_t)-1) return true;
}

? I can come up with a near continuum of cases that are slightly more or less "obvious" in their never-return. And the payoff? Not having an else. Lots of problems, little benefit.


Compilers have ad-hoc rules to detect unreachable code and the like. These don't have to be as formally specified as the standard, and they can differ from one compiler to another.

The standard, meanwhile, has to be the same for every compiler. And the rules for what is and isn't eliminated have to be identical.

The standard applies a simple rule; the if and else blocks are the only candidates for eliminatation.


So the standard doesn't do that. If you want code to be eliminated, put it in a if constexpr or else block of the if constexpr. Language development resources are better spent on things that have better yield and are less painful.

Lossa answered 17/12, 2018 at 16:1 Comment(1)
And here is the obligatory reference to the halting problem.Perry

© 2022 - 2024 — McMap. All rights reserved.