How to use sfinae to exclude types for which a function is defined?

202 Views Asked by At

Considering how the comma expression in a decltype() trailing return type can be used to check if a function can be applied:

template <class A>
auto f(A a) -> decltype(check_if_possible(a), return_type(a))

How can I negate the part before the comma to exclude cases in which check_if_possible(a) is defined?

Context: f() is an overloaded function for different A. I want to resolve an ambiguous overload between two implementations. One of them uses check_if_possible(a), the other works in cases where this cannot be applied to a.

Edit: Using std::enable_if and related things is welcome as well, but I would like to avoid additional helper functions / templates, because there are several functions similar to f() with different enable criteria. If this is not possible, then that is an answer as well ;)

2

There are 2 best solutions below

2
max66 On

How can I negate the part before the comma to exclude cases in which check_if_possible(a) is defined?

I don't know a way, but...

Context: f is an overloaded function for different A. I want to resolve an ambiguous overload between two implementations. One of them uses check_if_possible(a), the other works in cases where this cannot be applied to a.

I usually write a f() function for both cases that demand a couple of f_helper() functions receiving an additional argument.

For example

template <typename A>
auto f (A && a)
 { return f_helper(std::forward<A>(a), 0); }

Observe the last argument: 0, it's a int

Then you can write the check_if_possible() version, receiving an additional int

template <typename A>
auto f_helper (A a, int) -> decltype(check_if_possible(a), return_type(a))
 { /* ... */ }

and a generic version, always enabled, receiving an additional long

template <typename A>
auto f_helper (A a, long)
 { /* ... */ }

This way, when A support check_if_possible(), both f_helper() are enabled but the specific version is preferred because 0 is a int, the specific version receive an additional int (exact match) and the generic an additional long (int is convertible to long but isn't an exact match).

When A doesn't support check_if_possible(), only the generic f_helper() is available.

0
ロウリン On

You could rely on C++17's constexpr if in order to provide the two different implementations:

template<class A>
auto f(A a) {
   if constexpr (supportsExpression(a)) {
      // implementation that uses the check_if_possible(a) expression
      // ...
      check_if_possible(a);
      // ...
   } else {
      // implementation that doesn't use the check_if_possible(a) expression
      // ...
   }
}

Then, provide two overloaded function templates for supportsExpression():

// uses SFINAE
template<typename T>
constexpr auto supportsExpression(T&& arg) ->
   decltype((void)check_if_possible(arg), bool{})
{
   return true;
}

// fallback
template<typename... T>
constexpr bool supportsExpression(T&&...) { return false; }

The first overload will be – due to SFINAE – discarded if the expression check_if_possible(arg) wouldn't compile.

If the first overload is not discarded from the overloaded set due to SFINAE, we end up with two suitable overloads when calling supportsExpression() in f(). In this case, however, the non-variadic function template is preferred over the variadic one, i.e., the one that returns true (the type A supports the expression).