Is there a generic way to negate a decltype condition with SFINAE?

1k views Asked by At

I have a dozen functions or so that take two parameters: a generic, and a specific type. E.g.:

template <class A, class B>
void foo(A& a, B& b)
{
    cout << "generic fallback" << endl;
}

template <class A>
void foo(A& a, int &i)
{
    cout << "generic int" << endl;
}

template <class A>
void foo(A& a, string& s)
{
    cout << "generic str" << endl;
}

I want to create an overload which will get called whenever A is an instance of a particular struct[1]. The best I came up with so far was:

struct mine
{
    int is_special;
};

template <class A, class B>
auto foo(A& a, B& b) -> decltype(A::is_special, void())
{
    cout << "specialized fallback" << endl;
}

The result I want is:

int x;
string y;
float z;
string generic;
mine special;

foo(generic, x); // generic int
foo(generic, y); // generic string
foo(generic, z); // generic fallback
foo(special, x); // specialized fallback
foo(special, y); // specialized fallback
foo(special, z); // specialized fallback

The above code doesn't work, however, because for the specialized cases, there is an ambiguous overload. Is there any easy way to cause those functions to only get created if A::is_special is not a valid type? Ideally I would annotate each function with something like:

template <class A, class B>
auto foo(A& a, B& b) -> decltype(doesnt_work(A::is_special), void())
// ...

I ask also in the more general case: given any "positive" SFINAE test which results in a function or class getting created as a result of the test, is there any way to negate that test specifically for use in other cases? In essence, the equivalent of if ... else if with SFINAE.

I did get this case to work, but I had to rename all foo to foo_imp, add a long parameter to the generic ones, an int parameter to the specialized one, and then define a foo that called them (ideone code here). This seems less-than-ideal in that it's not as straightforward, although either way I have to modify all the existing foos anyway.


[1] Note that I can't use the type's name because it is a nested template and would thus result in a non-deducible context.

3

There are 3 answers

0
T.C. On BEST ANSWER

In essence, the equivalent of if ... else if with SFINAE.

You can manually control overload resolution with an extra parameter:

template<int I> struct rank : rank<I-1> { static_assert(I > 0, ""); };
template<> struct rank<0> {};

template <class A, class B>
auto foo(A& a, B& b, rank<10>) -> /*some SFINAE */
{
}

template <class A, class B>
auto foo(A& a, B& b, rank<9>) -> /* some other SFINAE */
{
}

// ...

template <class A, class B>
void foo(A& a, B& b, rank<0>)
{
// fallback
}

template <class A, class B>
void foo(A& a, B& b) { return foo(a, b, rank<20>()); }

The viable overload with the highest "rank" will be selected by overload resolution.

You can't directly negate an ad hoc SFINAE constraint ("this expression used in the signature must be well-formed"). You'll need to write an actual trait to detect it, and then negate the result. Simplest way to do it is probably by using std::experimental::is_detected_v, recently voted into v2 of the library fundamentals TS:

template<class T>
using is_special_t = decltype(T::is_special);

template <class A, class B>
auto foo(A& a, B& b) -> std::enable_if_t<!std::experimental::is_detected_v<is_special_t, A>>
{
    cout << "generic fallback" << endl;
}

template <class A, class B>   
auto foo(A& a, B& b) -> std::enable_if_t<std::experimental::is_detected_v<is_special_t, A>>
{
    cout << "specialized fallback" << endl;
}
1
davidhigh On

First, a template to check whether a given type is "special":

template<typename ...> using void_t = void;

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

template< typename T >
struct is_special<T, void_t<decltype(T::is_special)> > : public std::true_type {};

Instead of the void_t, you could also use your decltype(T::is_special, void() ) construct.

Next, you can plug in some SFINAE expressions which enable/disable the relevant functions depending on the passed types. The following code particularly disables any generic callback for your special type.

template <class A, class B, typename = std::enable_if_t<!is_special<A>::value> >
void foo(A& a, B& b)
{
    cout << "generic fallback" << endl;
}

template <class A, typename = std::enable_if_t<!is_special<A>::value>>
void foo(A& a, int &i)
{
    cout << "generic int" << endl;
}

template <class A, typename = std::enable_if_t<!is_special<A>::value>>
void foo(A& a, string& s)
{
    cout << "generic str" << endl;
}

//this SFINAE check is probably redundant    
template <class A, class B, typename = std::enable_if_t<is_special<A>::value> >   
auto foo(A& a, B& b)
{
    cout << "specialized fallback" << endl;
}

DEMO

Note that the last check for is_special in the specialized fallback is probably redundant.

0
Yakk - Adam Nevraumont On
template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;

std::void_t is not in C++11. The above will work in every C++11 compiler I've tried it on. (a one-step alias breaks some compilers due to a C++11 language defect)

namespace details {
  template<template<class...>class Z, class, class...Ts>
  struct can_apply : std::false_type {};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, /*std::*/ void_t<Z<Ts...>>, Ts...>: std::true_type {};
}
template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z, void, Ts...>;

this takes a template Z and a pack of parameters Ts..., and returns true_type iff Z<Ts...> is a valid expression.

template<class T>
using is_special_t = decltype( T::is_special );

takes a T, and returns its is_special type.

template<class T>
using has_special = can_apply< is_special_t, T >;

solves your problem.

This is similar in function to std::experimental::is_detected, but is an independent implementation.