SFINAE: "enable_if cannot be used to disable this declaration"
Asked Answered
C

3

22

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.

Costly answered 29/8, 2018 at 12:6 Comment(0)
L
28

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();
}
Loire answered 29/8, 2018 at 13:31 Comment(2)
The last example of MyQueue can also be MyQueueBase, so that class MyQueue : public MyQueueBase<Queue> could truly give the OP the conditional member they want, without repeating the class definition twice. I know I'm writing this under the answer of a standard library implementor, but I hope you will add it for the OP's benefit. Otherwise, fantastic answer.Cree
Yes, I'll add that, because that's how I'd do it in reality.Loire
S
12

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

void notify_exit() requires has_member_function_notify_exit<Queue, void>::value;
Shebat answered 29/8, 2018 at 13:13 Comment(1)
That's awesome to know. Right now thought I'm still on C++11- with MSVC 2013.Costly
C
4

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

Cree answered 29/8, 2018 at 12:13 Comment(4)
Hi, I'll give this a try ASAP (within the hour.) I'm actually unclear about the two problems you pointed out though. Why is the declaration is ill-formed? And I don't understand "we aren't resolving overloads when the class template is instantiated", what do you mean by that? I thought that's exactly what enable_if is for? Thanks for the help!Costly
@Costly - SFINAE isn't magic that removes declarations. It's a mechanic that allows ill-formed template functions to be silently ignored during overload resolution, in favor of more fitting overloads. Simply appearing as a member in a class, isn't something SFINAE is applicable for. When the class is instantiated, so are the declarations of the members. But the compiler can't instantiate a declaration for notify_exit, since there is no return type. It's simply ill-formed. The template declaration is valid, since now the enable if doesn't fail immediately. It only fails at the call site.Cree
It's more accurate to say that SFINAE disables things during template argument substitution (that's what the S in SFINAE stands for). That substitution is done during template argument deduction, which occurs during overload resolution. If a substitution failure happens, deduction fails, and the function can't be called. But in the OP's example there is no template argument deduction, the template arguments are explicitly provided: MyQueue<std::queue<int>>. Since you're not deducing the arguments, a substitution failure doesn't just cause deduction failure, it causes an error.Loire
I see, non-template member function of a template class requires no template argument deduction since template arguments are already given by class instantiation MyQueue<std::queue<int>>, by making that member function itself a template one, compiler cannot determine whether it's ill-formed at class instantiation stage because now has_member_function_notify_exit depends on Q which can only be determined at call site of notify_exit(). so enable_if must depending on template arguments from same template<> statementOneness

© 2022 - 2024 — McMap. All rights reserved.