Приоритет специализации шаблона SFINAE

#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;
}

Я ожидал, что третий шаблон будет более специализированным, чем второй, но я получил неоднозначную копию шаблона.

Есть ли способ сделать третий шаблон более специализированным? Явно проверяю, не будет ли работать для меня T std::array во втором шаблоне. Я пишу библиотеку и хотел бы, чтобы пользователи могли определить свои собственные специализаций trait. Второй шаблон предназначен для общей специализации для контейнеров в отсутствие более конкретного признака.

Ответ 1

#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;
}

РЕДАКТИРОВАТЬ

Во-первых, нет гарантии, что следующее будет более угадать, чем доказать. Возможно, кто-то другой может исправить, расширить копию, вставьте его или что-то еще.

Однако моя первая догадка, увидев вопрос, заключалась в использовании std::void_t. Я почти уверен, что раньше видел что-то подобное, но да, тоже не гарантировал бы этого. Чтобы показать, что можно использовать std::void_t, мы должны показать, что "одна специализация шаблона более конкретна, чем другая". И мы делаем это, проверяя частичный порядок. Я буду подражать приведенному выше со следующим, что немного короче.

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"; }
};

Я не собираюсь объяснять, как делается частичный заказ, займет слишком много времени. После преобразования в функции и т.д. Вы получите нечто похожее на следующее.

//#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

Теперь мы должны показать, что # 1 из # 2 работает. Если это так, мы показали, что №2 более специализирован.

//#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

Это в основном мой эскиз, не проверяя стандарт или что-то еще. Я уверен, вы можете найти его где-то в бесконечных линиях стандарта...

Если вы обратили внимание, вы заметите (*). Эта строка в основном является единственной важной, если вы хотите использовать decltype (...). Я предполагаю, что использование decltype (...) приводит к не выводимому контексту для правой стороны, что, возможно, не позволяет использовать T из вычитания P1/A1. Но да, это в основном причина, почему я не включил ответ сначала в рабочее решение std::void_t. Наконец, альтернативное определение std::void_t с typename... Я думаю, что не выводимый контекст тоже так же, как decltype (...), из-за части typename.


РЕДАКТИРОВАТЬ

Просто добавьте несколько окончательных строк. В принципе не должно быть проблем с decltype sfinae. Хорошо, что он не выводил контекст, но почему это проблема? Единственное, о чем я могу думать, это то, что в невыводимом контексте есть специальные правила в сочетании с частичным упорядочением...