Template template class predicate not working in partial specialization
Asked Answered
V

2

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?

Vicentevicepresident answered 28/11, 2018 at 6:38 Comment(4)
I cannot find the reason, but you need the partial specialization to be Resolve<Predicate, T, std::void_t<Predicate<T>>. Then at that point, you can also remove the std::void_t in the alias.Phore
btw, your code look a lot like the dectection idiomPhore
@GuillaumeRacicot But then what is the predicate? Just decltype()? Very nice! Also, confusing why the above approach doesn't work.Vicentevicepresident
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.Phore
P
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

Phore answered 28/11, 2018 at 6:58 Comment(6)
Have an upvote for the minimal change. But I deleted mine because I'm still guessing at the reasons.Abrahan
@StoryTeller Thanks. I haven't found the reason either. It might be because the rule doesn't make the specialization more specialized.Phore
Might be. I'm examining partial ordering at the moment to see why.Abrahan
@StoryTeller I added a link to another answer that explains void_t. However, even that answer don't seem to explain your case completely.Phore
This is an interesting take on what I was trying to do. Thanks.Vicentevicepresident
@Vicentevicepresident I was sure it was about std::void_t being an alias to void, and mixed that with template argument deduction for the template specialization, but every experimentation showed me the contrary. Sorry I couldn't find the exact reason, but it has to do with template partial ordering or something similar.Phore
V
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);
}
Vide answered 28/11, 2018 at 7:42 Comment(6)
So... why does it usually work with void_t? It's an alias template too.Abrahan
@StoryTeller Because the deduction starts from std::void_t<Predicate<T>>>, std::void_t is already known.Vide
Sorry, by usually I meant std:void_t<decltype(std::declval<T>().foo)>Abrahan
@StoryTeller decltype(std::declval<T>().foo) is a non-deduced context, compiler won't deduce from it.Vide
Yeah, it is (should really print that bullet list). But shouldn't the equivalence of alias template specializations make this moot? I.e., it should be as though the OP wrote directly std::void_t<declval<...>(...).foo>, no? Sorry for flood of questions, I was just staring at these passages for too long.Abrahan
Oh. no wait. I see now. Great answer! That void_t we hide is failing immediately because the specialization needs to be replaced by its equivalent type!Abrahan

© 2022 - 2024 — McMap. All rights reserved.