SFINAE template specialization precedence
Asked Answered
S

1

16
#include <iostream>
#include <array>
#include <vector>

template <typename T, typename SFINAE=void>
struct trait;

template <typename T>
struct trait<T, decltype(
  std::declval<const T&>().begin(),
  std::declval<const T&>().end(),
  void()
)> {
  static const char* name() { return "Container"; }
};

template <typename T, std::size_t N>
struct trait<std::array<T,N>> {
  static const char* name() { return "std::array"; }
};

int main(int argc, char* argv[]) {
  std::cout << trait<std::vector<int>>::name() << std::endl;
  std::cout << trait<std::array<int,2>>::name() << std::endl;
}

I was expecting the third template to be more specialized than the second, but I got an ambiguous template instantiation.

Is there a way to make the third template more specialized? Explicitly checking for whether T is an std::array in the second template won't work for me. I'm writing a library and would like users to be able to define their own specializations of the trait. The second template is intended to be a generic specialization for containers in absence of a more specific trait.

Sidetrack answered 23/9, 2018 at 2:14 Comment(8)
Add a reproducible example, please.Jackdaw
template <typename T, std::size_t N> struct trait<std::array<T,N>, void> { }; compiles for me, with gcc 8.1.1. Note the void.Souvenir
@SamVarshavchik I've tried that, but it doesn't change anything. I'm using gcc 7.2.0. I also tried gcc 8.1.0.Sidetrack
Godbolt seems to agree: godbolt.org/z/OhJXm2Sidetrack
Related: https://mcmap.net/q/391149/-partial-specialization-ordering-with-non-deduced-context/5376789, possibly caused by the same reason.Hoberthobey
The usual fix is to ship the SFINAE details into a different template.Valerlan
Just FYI: I would suggest using free std::begin and std::end functions as they are more generic (for example work for c-style arrays)Skier
@Skier Thanks! I've realized that since I posted the question.Sidetrack
S
8
#include <iostream>
#include <array>
#include <vector>

template <typename T, typename SFINAE=void>
struct trait;

template <typename T>
struct trait<T, std::void_t<decltype(std::declval<T>().begin()),
decltype(std::declval<T>().end())>> {
  static const char* name() { return "Container"; }
};

template <typename T, std::size_t N>
struct trait<std::array<T,N>,void> {
  static const char* name() { return "std::array"; }
};

int main(int argc, char* argv[]) {
  std::cout << trait<std::vector<int>>::name() << std::endl;
  std::cout << trait<std::array<int,2>>::name() << std::endl;
}

EDIT

First off, no guarantee for the following its really more a guess than a prove. Maybe someone else can correct, extend copy paste it or whatever.

However my first guess after seeing the question was to use std::void_t. I am pretty sure I have seen something like this before, but yeah would also not guarantee it. In order to show that std::void_t can be used we have to show that "one template specialization is more specific than the other". And we do this by checking partial order. I'll mimic the above with the following which is a bit more short.

template <typename T, typename SFINAE=void>
struct trait;
//#1
template <typename T>struct trait<T, std::void_t<decltype(std::declval<T>().begin())>>
{
  static const char* name() { return "Container"; }
};
//#2
template <typename T>struct trait<std::vector<T>,void> {
  static const char* name() { return "std::vector"; }
};

I am not going to explain how partial ordering is done, would take too long. After transforming to functions etc... you end up with something similar to the following.

//#2 from #1: f(trait<std::vector<T>,void>) from f(trait<__ANY_TYPE__, std::void_t<decltype(std::declval<__ANY_TYPE__>().begin())>)
    //P=trait<std::vector<T>,void>
    //A=trait<__ANY_TYPE__, std::void_t<decltype(std::declval<__ANY_TYPE__>().begin())>>
        //P1=std::vector<T>
        //A1=__ANY_TYPE__
        //P2=void
        //A2=std::void_t<decltype(std::declval<__ANY_TYPE__>().begin())>
        //==> T=? --> fail, #2 from #1 is not working

Now we have to show that #1 from #2 is working. If so we have shown that #2 is more specialized.

//#1 from #2: f(trait<T, std::void_t<decltype(std::declval<T>().begin())>>) from f(trait<std::vector<__ANY_TYPE__>,void>)
    //P=trait<T, std::void_t<decltype(std::declval<T>().begin())>>
    //A=trait<std::vector<__ANY_TYPE__>,void>
        //P1=T
        //A1=std::vector<__ANY_TYPE__>
        //P2=std::void_t<decltype(std::declval<T>().begin())> //(*)
        //A2=void
        //==> T=std::vector<__ANY_TYPE__> ok #1 from #2 works

Thats basically my sketch without checking the standard or anything else. I am pretty sure you can find it somewhere in the endless lines of the standard...

If you paid attention, you'll have noticed the (*). This line is basically the only important one if you want to use decltype(...). My guess is that using decltype(...) is leading to non-deduced context for the right hand side which is maybe not allowing to use the T from the P1/A1 deduction. But yeah this is basically the reason why i did not included an answer first to the working std::void_t solution. Finally the alternative std::void_t definition with typename ... is I think non-deduced context too just like decltype(...), due to the typename part.


EDIT

Just to add a few final lines. In principle there should not be a problem with decltype sfinae. Ok its non-deduced context, but why is it a problem? The only thing I can think of, is that non-deduced context has some special rules in combination with partial ordering...

Sleazy answered 23/9, 2018 at 3:57 Comment(6)
Interestingly, this works if void_t is defined as template <typename...> using void_t = void;, but not for template<typename... Ts> struct make_void { typedef void type; }; template<typename... Ts> using void_t = typename make_void<Ts...>::type;Sidetrack
We better wait for someone else to answer this. I am just too unsure and don't want to spread nonsense.Sleazy
@SU3, This does not work with template partial specialization of the form make_void<Types...>::type and decltype(std::declval<T>()/*...*/) because during the process of partial ordering of template class, this form of template argument will be none deduced context The non-deduced contexts are: (5.1) The nested-name-specifier of a type that was specified using a qualified-id. (5.2) The expression of a decltype-specifier.Imbecility
@Imbecility If you write that up as an answer, I'll accept it.Sidetrack
@Imbecility You are right. so this alternative form is like decltype(...) non-deduced context, in contrast to template <typename...> using void_t = void; which is not non-deduced context. But why is non-deduced context a problem here. In that sense that i can show that std::void_t is working but i cant show why decltype(...)` is not working.Sleazy
@Su3, I have doubts!Imbecility

© 2022 - 2024 — McMap. All rights reserved.