Метапрограммирование: отказ определения функции Определяет отдельную функцию

В этом ответе я определяю шаблон, основанный на свойстве is_arithmetic типа:

 template enable_if_t<is_arithmetic::value, string> stringify(T t){ return to_string(t); } template enable_if_t<!is_arithmetic::value, string> stringify(T t){ return static_cast(ostringstream() << t).str(); } 

dyp предполагает, что вместо свойства is_arithmetic этого типа определяется, является ли to_string для типа критерием выбора шаблона. Это явно желательно, но я не знаю, как сказать:

Если std::to_string не определен, используйте перегрузку ostringstream .

Объявление критериев to_string прост:

 template decltype(to_string(T{})) stringify(T t){ return to_string(t); } 

Это противоположность этим критериям, что я не могу понять, как построить. Это явно не работает, но, надеюсь, он передает то, что я пытаюсь построить:

 template enable_if_t (T t){ return static_cast(ostringstream() << t).str(); } 

Свежее голосование в основах библиотеки TS на собрании комитета на прошлой неделе:

 template using to_string_t = decltype(std::to_string(std::declval())); template using has_to_string = std::experimental::is_detected; 

Затем отправьте тег и / или SFINAE на has_to_string в has_to_string вашего сердца.

Вы можете проконсультироваться с текущим рабочим проектом TS о том, как is_detected и друзья могут быть реализованы. Это довольно похоже на can_apply в ответе @ Якка.

Использование void_t Уолтера Брауна :

 template  using void_t = void; 

Очень легко сделать такую ​​характеристику типа:

 template struct has_to_string : std::false_type { }; template struct has_to_string()))> > : std::true_type { }; 

Во-первых, я думаю, что SFINAE обычно следует скрывать от интерфейсов. Это делает интерфейс грязным. Поместите SFINAE с поверхности и используйте диспетчер меток для перегрузки.

Во-вторых, я даже скрываю SFINAE из classа признаков. Написание кода «могу ли я сделать X» достаточно распространено в моем опыте, что я не хочу писать грязный код SFINAE для этого. Поэтому вместо этого я пишу общий признак can_apply и имею свойство, которое SFINAE терпит неудачу, если переданы неправильные типы, используя decltype .

Затем мы передаем decltype declitype decltype с can_apply и can_apply истинный / ложный тип в зависимости от того, сбой приложения.

Это уменьшает работу на показатель «Я могу сделать X» до минимальной суммы и помещает несколько сложный и хрупкий код SFINAE в изо дня в день.

Я использую void_t C ++ 1z. Реализация его сама по себе легко (внизу этого ответа).

Metafunction, подобный can_apply , предлагается для стандартизации в C ++ 1z, но он не такой стабильный, как void_t , поэтому я его не использую.

Во-первых, пространство имен details чтобы скрыть реализацию can_apply от случайного обнаружения:

 namespace details { templateclass Z, class, class...> struct can_apply:std::false_type{}; templateclass Z, class...Ts> struct can_apply>, Ts...>: std::true_type{}; } 

Затем мы можем написать can_apply с точки зрения details::can_apply , и он имеет более приятный интерфейс (для него не требуется details::can_apply лишней void ):

 templateclass Z, class...Ts> using can_apply=details::can_apply; 

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

 template using to_string_t = decltype( std::to_string( std::declval() ) ); template using can_to_string = can_apply< to_string_t, T >; 

и у нас есть свойство can_to_string , которое истинно, если мы можем to_string a T

Для работы требуется написать новую черту, которая теперь составляет 2-4 строки простого кода – просто сделайте decltype using псевдонима, а затем выполните тест can_apply .

Как только мы это получим, мы используем отправку тегов для правильной реализации:

 template std::string stringify(T t, std::true_type /*can to string*/){ return std::to_string(t); } template std::string stringify(T t, std::false_type /*cannot to string*/){ return static_cast(ostringstream() << t).str(); } template std::string stringify(T t){ return stringify(t, can_to_string{}); } 

Весь уродливый код скрывается в пространстве имен details .

Если вам нужен void_t , используйте это:

 templatestruct voider{using type=void;}; templateusing void_t=typename voider::type; 

который работает в большинстве основных компиляторов C ++ 11.

Обратите внимание, что более простой templateusing void_t=void; не работает в некоторых старых компиляторах C ++ 11 (в стандарте была двусмысленность).

Вы можете написать вспомогательную черту для этого, используя выражение SFINAE:

 namespace detail { //base case, to_string is invalid template  auto has_to_string_helper (...) //... to disambiguate call -> false_type; //true case, to_string valid for T template  auto has_to_string_helper (int) //int to disambiguate call -> decltype(std::to_string(std::declval()), true_type{}); } //alias to make it nice to use template  using has_to_string = decltype(detail::has_to_string_helper(0)); 

Затем используйте std::enable_if_t::value>

демонстрация

Я думаю, что есть две проблемы: 1) Найти все жизнеспособные алгоритмы для данного типа. 2) Выберите лучший.

Мы можем, например, вручную указать порядок для набора перегруженных алгоритмов:

 namespace detail { template std::string stringify(choice<0>, T&& t) { using std::to_string; return to_string(std::forward(t)); } template std::string stringify(choice<1>, char const(&arr)[N]) { return std::string(arr, N); } template std::string stringify(choice<2>, T&& t) { std::ostringstream o; o << std::forward(t); return std::move(o).str(); } } 

Первый параметр функции указывает порядок между этими алгоритмами («первый выбор», «второй выбор», ..). Чтобы выбрать алгоритм, мы просто отправляем в наилучшее жизнеспособное соответствие:

 template auto stringify(T&& t) -> decltype( detail::stringify(choice<0>{}, std::forward(t)) ) { return detail::stringify(choice<0>{}, std::forward(t)); } 

Как это реализовано? Мы крадем немного от Xeo @ Flaming Dangerzone, а Paul @ void_t «может реализовать концепции»? (с использованием упрощенных реализаций):

 constexpr static std::size_t choice_max = 10; template struct choice : choice { static_assert(N < choice_max, ""); }; template<> struct choice {}; #include  template struct models : std::false_type {}; template struct models()...), void())> : std::true_type {}; #define REQUIRES(...) std::enable_if_t::value>* = nullptr 

Классы выбора наследуют от худших вариантов: choice<0> наследует choice<1> . Поэтому для аргумента choice<0> типа choice<0> параметр функции choice<0> типа choice<0> является лучшим совпадением, чем choice<1> , что лучше, чем choice<2> и т. Д. [Over.ics.rank ] P4.4

Обратите внимание, что более специализированный тай-брейкер применяется только в том случае, если ни одна из двух функций не лучше. Из-за общего порядка choice s мы никогда не столкнемся с этой ситуацией. Это предотвращает неоднозначность вызовов, даже если несколько алгоритмов жизнеспособны.

Мы определяем наши черты типа:

 #include  #include  namespace helper { using std::to_string; struct has_to_string { template auto requires_(T&& t) -> decltype( to_string(std::forward(t)) ); }; struct has_output_operator { std::ostream& ostream(); template auto requires_(T&& t) -> decltype(ostream() << std::forward(t)); }; } 

Макросов можно избежать, используя идею Р. Мартиньо Фернандеса :

 template using requires = std::enable_if_t::value, int>; // exemplary application: template = 0> std::string stringify(choice<0>, T&& t) { using std::to_string; return to_string(std::forward(t)); } 

Ну, вы можете просто пропустить всю магию метапрограммирования и использовать fit::conditional адаптер из библиотеки Fit :

 FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional( [](auto x) -> decltype(to_string(x)) { return to_string(x); }, [](auto x) -> decltype(static_cast(ostringstream() << x).str()) { return static_cast(ostringstream() << x).str(); } ); 

Или даже более компактный, если вы не возражаете против макросов:

 FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional( [](auto x) FIT_RETURNS(to_string(x)), [](auto x) FIT_RETURNS(static_cast(ostringstream() << x).str()) ); 

Обратите внимание, что я также ограничил вторую функцию, поэтому, если тип не может быть вызван с помощью to_string или не ostringstream в ostringstream тогда функция не может быть вызвана. Это помогает с улучшенными сообщениями об ошибках и лучшей совместимостью с проверкой требований типа.

  • Могут ли использоваться шаблоны lambda?
  • Проверьте, имеет ли class функцию-член определенной подписи
  • Как создать функцию шаблона внутри classа? (C ++)
  • Определить, является ли тип указателем в функции шаблона
  • «Не объявлена ​​в этой области» ошибка с шаблонами и наследованием
  • Как инициализировать std :: array элегантно, если T не является конструктивным по умолчанию?
  • использование имени classа в шаблоне classа без параметров шаблона
  • Вывод типа параметра шаблона c ++
  • Почему реализация и объявление classа шаблона должны быть в одном заголовочном файле?
  • Функция шаблона C ++ компилируется в заголовке, но не выполняется
  • Bash Templating: Как создать файлы конфигурации из шаблонов с Bash?
  • Давайте будем гением компьютера.