SFINAE: “enable_if cannot be used to disable this

2019-07-21 15:26发布

Why can I not use enable_if in the following context?

I'd like to detect whether my templated object has the member function notify_exit

template <typename Queue>
class MyQueue
{
   public:
    auto notify_exit() -> typename std::enable_if<
            has_member_function_notify_exit<Queue, void>::value,
            void
        >::type;

    Queue queue_a;
};

Initialised with:

MyQueue<std::queue<int>> queue_a;

I keep getting (clang 6):

example.cpp:33:17: error: failed requirement 'has_member_function_notify_exit<queue<int, deque<int, allocator<int> > >, void>::value';
      'enable_if' cannot be used to disable this declaration
            has_member_function_notify_exit<Queue, void>::value,

or (g++ 5.4):

In instantiation of 'class MyQueue<std::queue<int> >':
33:35:   required from here
22:14: error: no type named 'type' in 'struct std::enable_if<false, void>'

I've tried a bunch of different things, but can't figure out why I can't use enable_if to disable this function. Isn't this exactly what enable_if is for?

I've put a full example here (and cpp.sh link that often fails)

I've found similar Q/As on SO, but generally those were more complicated and attempting something different.

3条回答
疯言疯语
2楼-- · 2019-07-21 15:51

Instantiating a template causes the member instantiation of all the declarations it contains. The declaration you provide is simply ill-formed at that point. Furthermore, SFINAE doesn't apply here, since we aren't resolving overloads when the class template is instantiated.

You need to make the member into something with a valid declaration and also make sure the check is delayed until overload resolution. We can do both by making notify_exit a template itself:

template<typename Q = Queue>
auto notify_exit() -> typename std::enable_if<
        has_member_function_notify_exit<Q, void>::value,
        void
    >::type;

A working cpp.sh example

查看更多
3楼-- · 2019-07-21 15:54

When you instantiate MyQueue<std::queue<int>> the template argument std::queue<int> gets substituted into the class template. In the member function declaration that leads to a use of typename std::enable_if<false, void>::type which does not exist. That's an error. You can't declare a function using a type that doesn't exist.

Correct uses of enable_if must depend on a template parameter that is deduced. During template argument deduction, if substituting the deduced template argument for the template parameter fails (i.e. a "substitution failure") then you don't get an immediate error, it just causes deduction to fail. If deduction fails, the function isn't a candidate for overload resolution (but any other overloads will still be considered).

But in your case the template argument is not deduced when calling the function, it's already known because it comes from the surrounding class template. That means that substitution failure is an error, because the function's declaration is ill-formed before you even try to perform overload resolution to call it.

You can fix your example by turning the function into a function template, so it has a template parameter that must be deduced:

template<typename T = Queue>
  auto notify_exit() -> typename std::enable_if<
              has_member_function_notify_exit<T, void>::value,
              void
          >::type;

Here the enable_if condition depends on T instead of Queue, so whether the ::type member exists or not isn't known until you try to substitute a template argument for T. The function template has a default template argument, so that if you just call notify_exit() without any template argument list, it's equivalent to notify_exit<Queue>(), which means the enable_if condition depends on Queue, as you originally wanted.

This function can be misused, as callers could invoke it as notify_exit<SomeOtherType>() to trick the enable_if condition into depending on the wrong type. If callers do that they deserve to get compilation errors.

Another way to make the code work would be to have a partial specialization of the entire class template, to simply remove the function when it's not wanted:

template <typename Queue,
          bool Notifiable
            = has_member_function_notify_exit<Queue, void>::value>
class MyQueue
{
  public:
    void notify_exit();

    Queue queue_a;
};

// partial specialization for queues without a notify_exit member:
template <typename Queue>
class MyQueue<Queue, false>
{
  public:
    Queue queue_a;
};

You can avoid repeating the whole class definition twice in a few different ways. You could either hoist all the common code into a base class and only have the notify_exit() member added in the derived class that depends on it. Alternatively you can move just the conditional part into a base class, for example:

template <typename Queue,
          bool Notifiable
            = has_member_function_notify_exit<Queue, void>::value>
class MyQueueBase
{
  public:
    void notify_exit();
};

// partial specialization for queues without a notify_exit member:
template <typename Queue>
class MyQueueBase<Queue, false>
{ };

template<typename Queue>
class MyQueue : public MyQueueBase<Queue>
{
public:
  // rest of the class ...

  Queue queue_a;
};

template<typename Queue, bool Notifiable>
void MyQueueBase<Queue, Notifiable>::notify_exit()
{
  static_cast<MyQueue<Queue>*>(this)->queue_a.notify_exit();
}
查看更多
走好不送
4楼-- · 2019-07-21 15:54

With C++20 and concept, you may use requires:

void notify_exit() requires has_member_function_notify_exit<Queue, void>::value;
查看更多
登录 后发表回答