Konstantin Vladimirov
Konstantin Vladimirov

Reputation: 6999

Why SFINAE works incorrect for me in this case and how to fix it?

I am trying to leave in struct A one function foo (prints 0) if its parameter have template method isA<void> and another (prints 1) if haven't. This code (reduced to minimal example below) compiles (tried with gcc 6.1.0 and clang-3.9.0 with explicit --std=c++14 option) and runs.

But it prints 1, though, I am sure, that it shall print 0. I wonder where am I wrong, but real question is: how to make this work correct?

Please only C++14 solutions.

#include <type_traits>
#include <iostream>
#include <utility>

using std::enable_if;
using std::declval;
using std::true_type;
using std::false_type;
using std::cout;

template<int M>
struct ObjectX
{
  template<typename C>
  bool isA() { return false; }
};

struct XX : ObjectX<23456> {
  int af;
};

template <typename ObjType> using has_dep = decltype(declval<ObjType>().template isA<void>());

template <typename, typename = void>
struct has_isa : public false_type {};

template <typename ObjType>
struct has_isa<ObjType, has_dep<ObjType> > : public true_type {};

template<typename ObjType>
struct A
{
  template<typename T = void>
  typename enable_if<has_isa<ObjType>::value, T>::type
  foo() {
    cout << "called foo #0" << "\n";
  }

  template<typename T = void>
  typename enable_if<!has_isa<ObjType>::value, T>::type
  foo() {
    cout << "called foo #1" << "\n";
  }
};

int
main()
{
  A<XX> axx;
  // XX().template isA<void>(); -- to check, that we can call it and it exists
  axx.foo();
  return 0;
}

Upvotes: 3

Views: 115

Answers (3)

Guillaume Racicot
Guillaume Racicot

Reputation: 41780

Your sfinae fails because has_isa chooses the wrong specialization.

The use of has_isa<T> must be either the default implementation or the specialized version.

As you defined, you have a default argument to void:

//   default argument ---------v
template <typename, typename = void>
struct has_isa : public false_type {};

Then in the expression has_isa<T>, the second parameter must be void. It's roughly the same as writing has_isa<T, void>.

The problem is this:

template <typename ObjType>
struct has_isa<ObjType, has_dep<ObjType>> : public true_type {};
//                      ^--- what's that type?

Even though template partial ordering would consider this "overload" more specialized, it won't be chosen. Look at the definition of has_dep:

struct XX {
  template<typename C> bool isA() { return false; }
};

template <typename ObjType>
using has_dep = decltype(declval<ObjType>().template isA<void>());

Hey, that type has_dep<T> is the return type of t.isA<void>() which is bool!

So the specialized version look like this:

template <typename ObjType>
struct has_isa<ObjType, has_dep<ObjType>> : public true_type {};
//                      ^--- really, this is bool in our case

So in order for this to work, you must call has_isa<T, bool>. As this is impractical, you should define your specialization as this:

template <typename ObjType>
struct has_isa<ObjType, void_t<has_dep<ObjType>>> : public true_type {};

Where void_t is defined as so:

template<typename...>
using void_t = void; // beware for msvc

As such, has_isa<T> will always consider the specialization, because we send void as the second template parameter, and now our specialization always result with void as second parameter.


Also, as stated by Barry, your function is not correctly formed as sfinae only appears in immediate context. You should write it like this:

template<typename T = ObjType>
typename enable_if<has_isa<T>::value, void>::type
foo() { //                 ^--- sfinae happens with T
  cout << "called foo #0" << "\n";
}

If you don't wish to expose the template parameter, simply make the function private:

template<typename ObjType>
struct A {
public:
    void foo() {
        foo_impl();
    }

private:
    template<typename T = ObjType>
    typename enable_if<has_isa<T>::value, void>::type
    foo_impl() {
      cout << "called foo #0" << "\n";
    }

    template<typename T = ObjType>
    typename enable_if<!has_isa<T>::value, void>::type
    foo_impl() {
      cout << "called foo #1" << "\n";
    }
};

Upvotes: 2

Barry
Barry

Reputation: 303087

There are two problems in this program.


First, has_dep<XX> is bool. When we try has_dep<XX>, adding the default template arguments means this is really has_dep<XX, void>. But the specialization is has_dep<XX, bool> - which doesn't match what we're actually looking up. bool does not match void. That's why has_dep<XX> is false_type. The solution to this is std::void_t, and I'd suggest reading through that Q/A to get an idea for why it works. In your specialization, you need to use void_t<has_dep<ObjType>> instead.


Second, this is not right:

template<typename T = void>
typename enable_if<has_isa<ObjType>::value, T>::type

SFINAE only happens in the immediate context of substitution, and class template parameters are not in the immediate context of function template substitution. The right pattern here is:

template <typename T = ObjType> // default to class template parameter
enable_if_t<has_isa<T>>         // use the function template parameter to SFINAE
foo() { ... }

Make those two fixes, and the program works as intended.

Upvotes: 4

Jarod42
Jarod42

Reputation: 217283

Your problem is that you specialize the wrong class:

You should force has_dep to return void.

template <typename ObjType> using has_dep = decltype(static_cast<void>(declval<ObjType>().template isA<void>()));

So here

template <typename ObjType>
struct has_isa<ObjType, has_dep<ObjType> > : public true_type {};
// It is really <bjType, void> you specialize.

Upvotes: 1

Related Questions