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.
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:A working cpp.sh example
When you instantiate
MyQueue<std::queue<int>>
the template argumentstd::queue<int>
gets substituted into the class template. In the member function declaration that leads to a use oftypename 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:
Here the
enable_if
condition depends onT
instead ofQueue
, so whether the::type
member exists or not isn't known until you try to substitute a template argument forT
. The function template has a default template argument, so that if you just callnotify_exit()
without any template argument list, it's equivalent tonotify_exit<Queue>()
, which means theenable_if
condition depends onQueue
, as you originally wanted.This function can be misused, as callers could invoke it as
notify_exit<SomeOtherType>()
to trick theenable_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:
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:With C++20 and concept, you may use
requires
: