Giulio Franco
Giulio Franco

Reputation: 3230

C++ function which is only enabled if it doesn't exist

I think I just made a C++ paradox...

#include <type_traits>
#include <utility>

// If has_f trait is defined in this way, compilation breaks because of infinite recursion in template substitution
/*
template< typename T, typename Enable=void >
struct has_f : std::false_type { };

template< typename T >
struct has_f<T, decltype(f(std::declval<T>()))> : std::true_type { };
*/

// Defining has_f like this works on MSVC, gcc and CLang
namespace has_f_impl {
  struct no{ };
  template< typename T >
  no check(...);
  template< typename T >
  decltype(f(std::declval<T>())) check(void const*);

  template< typename T >
  struct has_f : std::integral_constant<bool, !std::is_same<no, decltype(check<T>(nullptr))>::value> { };
}
using has_f_impl::has_f;

struct Foo { };
struct Bar { };

template< typename T, std::enable_if_t<std::is_same<Foo, T>::value, int> = 0 >
void f(T const&);

template< typename T, std::enable_if_t<!has_f<T const&>::value, int> = 1 >
void f(T const&);

int main() {
  f(Foo()); // Calls f<Foo,0>()
  f(Bar()); // Calls f<Bar,1>()
  f(Foo()); // Calls f<Foo,0>()
  f(Bar()); // Calls f<Bar,1>()
}

The above code surprisingly works, and in a very smart way, only using the generic f when there's really no other option.

Also, and this is probably because of ODR, the following happens

// Includes, has_f, Foo, Bar and f as above
template< typename T, std::enable_if_t<has_f<T const&>::value>* = nullptr >
void g(T const&);

int main() {
  f(Foo()); // Calls f<Foo,0>()
  f(Bar()); // Calls f<Bar,1>()
  f(Foo()); // Calls f<Foo,0>()
  f(Bar()); // Calls f<Bar,1>()
  g(Foo());
  //g(Bar()); //No such function
}

As far as I tried, all of this seems to be independent from declaration order.

My question is: what's really happening here? Is this a standard-defined behavior, an undefined condition which all the compiler I tried handle in the same way, or a bug which is present by coincidence in all the compilers I tried?

Upvotes: 3

Views: 251

Answers (1)

Barry
Barry

Reputation: 303537

I suspect this is all simply covered by [temp.inst]:

The result of an infinite recursion in instantiation is undefined.

Regardless of which way you define your has_f, it involves infinite recursion. has_f<Bar> involves the instantiation of f(Bar ) which involves the instantiation of has_f<Bar> which involves the instantiation of ...

The fact that one way of defining has_f works in some circumstances but not in others, and the other way definitely doesn't work, is just a consequence of undefined behavior. Undefined behavior is undefined.

Upvotes: 2

Related Questions