if constexpr vs sfinae
Asked Answered
M

2

11

With the introduction of if constexpr in c++17, some problems which were solved by using compile-time SFINAE in c++14/c++11 can now be solved using if constexpr, with an easier syntax.

Consider, e.g., the following basic example of a compile-time recursion to produce a subroutine which prints a variable number of arguments.

#include <iostream>
#include <type_traits>

template <typename T>
void print_sfinae(T&& x)
{
  std::cout << x << std::endl;
}

template <typename T0, typename... T>
std::enable_if_t<(sizeof...(T) > 0)> print_sfinae(T0&& x, T&&... rest)
{
  std::cout << x << std::endl;
  print_sfinae(std::forward<T>(rest)...);
}

template <typename T0, typename... T>
void print_ifconstexpr(T0&&x, T&&... rest)
{
  if constexpr (sizeof...(T) > 0)
         {
            std::cout << x << std::endl;
            print_ifconstexpr(std::forward<T>(rest)...);
         }
  else
      std::cout << x << std::endl;
}

int main()
{
  print_sfinae(5, 2.2, "hello");
  print_ifconstexpr(5, 2.2, "hello");

  return 0;
}

The routine print_sfinae uses SFINAE techniques from c++11, whereas print_ifconstexpr does the same job by using if constexpr.

Can one assume that the compiler, on evaluating the if constexpr completely discards the non-verified condition and generate code only for the branch which satisfy the if constexpr condition? Does the standard specifies such a behavior for the compiler?

More generally, in terms of efficiency and generated code, are solutions based on if constexpr identical to the equivalent solutions based on pre-c++17 SFINAE?

Monovalent answered 6/1, 2019 at 20:6 Comment(3)
Not what you asked about, but variadic print can be written as a non-recursive one-liner if you use C++17 fold expressions. Pre-C++17 the same thing could be accomplished using the dummy array trick.Liquor
@Liquor dummy array trick = ?Goldsmith
@Walter: int dummy[] = {0, ((std::cout << xs << std::endl), 0)...}; static_cast<void>(dummy); // Avoid warning for unused varJarret
M
11

Can one assume that the compiler, on evaluating the if constexpr completely discards the non-verified condition and generate code only for the branch which satisfy the if constexpr condition? Does the standard specifies such a behavior for the compiler?

The standard specifies that, from [stmt.if]:

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

The point here is that the discard statement is not instantiated - this is the whole purpose behind if constexpr as a language feature, to allow you to write:

template <typename T0, typename... T>
void print_ifconstexpr(T0&& x, T&&... rest)
{
    std::cout << x << std::endl;
    if constexpr (sizeof...(T) > 0) {
        print_ifconstexpr(std::forward<T>(rest)...);
    }
}

You can't do that with a simple if, because that would still require instantiating the substatements - even if the condition can be determined to be false at compile time. A simple if would require the ability to call print_ifconstexpr().

if constexpr will not instantiate the recursive call unless there is something in rest..., so this works.

Everything else follows from the lack of instantiation. There can't be any generated code for the discarded statement.

The if constexpr form is easier to write, easier to understand, and surely compiles faster. Definitely prefer it.


Note that your first example doesn't need the SFINAE at all. This works just fine:

template <typename T>
void print(T&& x)
{
    std::cout << x << std::endl;
}

template <typename T0, typename... T>
void print(T0&& x, T&&... rest)
{
    std::cout << x << std::endl;
    print(std::forward<T>(rest)...);
}

As does:

void print() { }

template <typename T0, typename... T>
void print(T0&& x, T&&... rest)
{
    std::cout << x << std::endl;
    print(std::forward<T>(rest)...);
}
Metempsychosis answered 6/1, 2019 at 20:18 Comment(0)
S
0

C++ specifies program observable behaviour.

Both pieces of code have as observable behaviour printing.

Copying references, calling functions that take references and return void, are both not observable behaviour.

Both functions have identical obserable behaviour. So the C++ standard has zilch to say about any differences in runtime, code size or memory usabe between them.

Shona answered 6/1, 2019 at 23:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.