7

I have many EnableIf traits that basically check whether the input type satisfies an interface. I was trying to create a generic Resolve trait that can be used to transform those into a boolean trait.

Something like this - https://wandbox.org/permlink/ydEMyErOoaOa60Jx

template <
  template <typename...> class Predicate,
  typename T,
  typename = std::void_t<>>
struct Resolve : std::false_type {};
template <template <typename...> class Predicate, typename T>
struct Resolve<Predicate, T, Predicate<T>> : std::true_type {};

Now if you have an EnableIf trait like so

template <typename T>
using EnableIfHasFoo = std::void_t<decltype(std::declval<T>().foo())>;

You can create a boolean version of that very quickly

template <typename T>
struct HasFoo : Resolve<EnableIfHasFoo, T> {};

Or the analogous variable template.

But for some reason the partial specialization is not working as expected. Resolve does not work as intended. See the output here - https://wandbox.org/permlink/ydEMyErOoaOa60Jx. The same thing implemented "manually" works - https://wandbox.org/permlink/fmcFT3kLSqyiBprm

I am resorting to manually defining the types myself. Is there a detail with partial specializations and template template arguments that I am missing?

Curious
  • 20,870
  • 8
  • 61
  • 146
  • 1
    I cannot find the reason, but you need the partial specialization to be `Resolve>`. Then at that point, you can also remove the `std::void_t` in the alias. – Guillaume Racicot Nov 28 '18 at 06:47
  • btw, your code look a lot like the [dectection idiom](https://en.cppreference.com/w/cpp/experimental/is_detected) – Guillaume Racicot Nov 28 '18 at 06:49
  • @GuillaumeRacicot But then what is the predicate? Just decltype()? Very nice! Also, confusing why the above approach doesn't work. – Curious Nov 28 '18 at 06:49
  • 1
    It can be an alias template to simply `decltype`. I don't know why, but the `void_t` must be in the partial specialization. I think it has to do with how partial ordering work. – Guillaume Racicot Nov 28 '18 at 06:52

2 Answers2

5

I cannot find the exact reason why your example don't work. If you want to dig more into the details of std::void_t, here's an interesting explanation

Even if I cannot explain it in depth, I would like to add another reliable syntax that is used in the detection idiom.

template<
    template <typename...> class Predicate,
    typename T,
    typename = void>
struct Resolve : std::false_type {};

template <template <typename...> class Predicate, typename T>
struct Resolve<Predicate, T, std::void_t<Predicate<T>>> : std::true_type {};

template <typename T>
using EnableIfHasFoo = decltype(std::declval<T>().foo());

live on compiler explorer

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
5

The reason why your approach will fail is that Predicate<T>> in the third template parameter is not a non-deduced context. This causes the deduction to directly fail (see [temp.alias]/2), instead of using the deduced template arguments from elsewhere as in a non-deduced context.

You can wrap your Predicate<T>> to a non-deduced context to make it work:

template<class T>
struct identity {
    using type = T;
};

template <template <typename...> class Predicate, typename T>
struct Resolve<Predicate, T, typename identity<Predicate<T>>::type> : std::true_type {};

Live Demo

Because inside a non-deduced context, the deduction won't happen for Predicate<T> part, instead, it will use the Predicate and T obtained from elsewhere.

As for why the usual detection-idiom (see Guillaume Racicot's answer) will work, it is because std::void_t as a template alias, will be replaced by void in deduction phase (see [temp.alias]/2), thus no deduction will happen.

Here are some examples to illustrate it more clearly:

template<class T>
using always_int = int;

template<template<class> class TT>
struct deductor {};

template<template<class> class TT, class T>
void foo(T, deductor<TT>) {}

template<template<class> class TT, class T>
void bar(T, deductor<TT>, TT<T>) {}

template<class T>
void baz(T, always_int<T>) {}

int main() {
    // ok, both T and TT are deduced
    foo(0, deductor<always_int>{});

    // ERROR, TT<T> is NOT a non-deduced context, deduction failure
    bar(0, deductor<always_int>{}, 0);

    // ok, T is deduced, always_int<T> is replaced by int so no deduction
    baz(0, 0);
}
llllllllll
  • 16,169
  • 4
  • 31
  • 54