Метапрограммирование: отказ определения функции Определяет отдельную функцию
В этот ответ Я определяю шаблон, основанный на свойстве типа is_arithmetic
:
template<typename T> enable_if_t<is_arithmetic<T>::value, string> stringify(T t){
return to_string(t);
}
template<typename T> enable_if_t<!is_arithmetic<T>::value, string> stringify(T t){
return static_cast<ostringstream&>(ostringstream() << t).str();
}
dyp предлагает, что вместо свойства is_arithmetic
этого типа определяется, является ли to_string
для типа критерием выбора шаблона. Это явно желательно, но я не знаю, как сказать:
Если std::to_string
не определено, используйте перегрузку ostringstream
.
Объявление критериев to_string
прост:
template<typename T> decltype(to_string(T{})) stringify(T t){
return to_string(t);
}
Это противоположность этим критериям, что я не могу понять, как построить. Это явно не работает, но, надеюсь, он передает то, что я пытаюсь построить:
template<typename T> enable_if_t<!decltype(to_string(T{})::value, string> (T t){
return static_cast<ostringstream&>(ostringstream() << t).str();
}
Ответы
Ответ 1
Недавно проголосовали за фундаментальные основы библиотеки TS на собрании комитета на прошлой неделе:
template<class T>
using to_string_t = decltype(std::to_string(std::declval<T>()));
template<class T>
using has_to_string = std::experimental::is_detected<to_string_t, T>;
Затем отправьте отправку тега и/или SFINAE на has_to_string
на ваш сердечный контент.
Вы можете запросить текущий рабочий проект TS о том, как is_detected
и друзья могут быть реализованы. Это довольно похоже на can_apply
в ответе @Yakk.
Ответ 2
Использование Уолтер Браун void_t
:
template <typename...>
using void_t = void;
Очень легко сделать такую характеристику типа:
template<typename T, typename = void>
struct has_to_string
: std::false_type { };
template<typename T>
struct has_to_string<T,
void_t<decltype(std::to_string(std::declval<T>()))>
>
: std::true_type { };
Ответ 3
Во-первых, я думаю, что SFINAE обычно скрывается от интерфейсов. Это делает интерфейс грязным. Поместите SFINAE с поверхности и используйте диспетчер меток для перегрузки.
Во-вторых, я даже скрываю SFINAE из класса признаков. Написание кода "могу ли я сделать X" достаточно распространено в моем опыте, что я не хочу писать грязный код SFINAE для этого. Поэтому вместо этого я пишу общий признак can_apply
и имею свойство, которое SFINAE терпит неудачу, если передать неправильные типы, используя decltype
.
Затем мы передаем символ decltype
с ошибкой SFIANE на can_apply
и выберем тип true/false в зависимости от того, сбой приложения.
Это уменьшает работу на показатель "Я могу сделать X" до минимальной суммы и помещает несколько сложный и хрупкий код SFINAE в изо дня в день.
Я использую С++ 1z void_t
. Реализация его сама по себе легко (внизу этого ответа).
Для стандартизации в С++ 1z предлагается метафокус, аналогичный can_apply
, но он не такой стабильный, как void_t
, поэтому я его не использую.
Во-первых, пространство имен details
, чтобы скрыть выполнение can_apply
от случайного обнаружения:
namespace details {
template<template<class...>class Z, class, class...>
struct can_apply:std::false_type{};
template<template<class...>class Z, class...Ts>
struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:
std::true_type{};
}
Затем мы можем написать can_apply
в терминах details::can_apply
, и он имеет более удобный интерфейс (для него не требуется передача дополнительного void
):
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z, void, Ts...>;
Выше приведен общий код метапрограммирования помощника. Как только мы получим его, мы можем написать класс признаков can_to_string
очень чисто:
template<class T>
using to_string_t = decltype( std::to_string( std::declval<T>() ) );
template<class T>
using can_to_string = can_apply< to_string_t, T >;
и мы имеем черту can_to_string<T>
, которая истинна, если мы можем to_string
a T
.
Для работы требуется написать новый признак, который теперь составляет 2-4 строки простого кода - просто создайте псевдоним decltype
using
, а затем выполните тест can_apply
.
Как только мы это получим, мы используем отправку тегов для правильной реализации:
template<typename T>
std::string stringify(T t, std::true_type /*can to string*/){
return std::to_string(t);
}
template<typename T>
std::string stringify(T t, std::false_type /*cannot to string*/){
return static_cast<ostringstream&>(ostringstream() << t).str();
}
template<typename T>
std::string stringify(T t){
return stringify(t, can_to_string<T>{});
}
Весь уродливый код скрывается в пространстве имен details
.
Если вам нужен void_t
, используйте это:
template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;
который работает в большинстве основных компиляторов С++ 11.
Обратите внимание, что более простой template<class...>using void_t=void;
не работает в некоторых старых компиляторах С++ 11 (в стандарте была двусмысленность).
Ответ 4
Вы можете написать вспомогательную черту для этого, используя выражение SFINAE:
namespace detail
{
//base case, to_string is invalid
template <typename T>
auto has_to_string_helper (...) //... to disambiguate call
-> false_type;
//true case, to_string valid for T
template <typename T>
auto has_to_string_helper (int) //int to disambiguate call
-> decltype(std::to_string(std::declval<T>()), true_type{});
}
//alias to make it nice to use
template <typename T>
using has_to_string = decltype(detail::has_to_string_helper<T>(0));
Затем используйте std::enable_if_t<has_to_string<T>::value>
Демо
Ответ 5
Я думаю, что есть две проблемы: 1) Найти все жизнеспособные алгоритмы для данного типа. 2) Выберите лучший.
Мы можем, например, вручную указать порядок для набора перегруженных алгоритмов:
namespace detail
{
template<typename T, REQUIRES(helper::has_to_string(T))>
std::string stringify(choice<0>, T&& t)
{
using std::to_string;
return to_string(std::forward<T>(t));
}
template<std::size_t N>
std::string stringify(choice<1>, char const(&arr)[N])
{
return std::string(arr, N);
}
template<typename T, REQUIRES(helper::has_output_operator(T))>
std::string stringify(choice<2>, T&& t)
{
std::ostringstream o;
o << std::forward<T>(t);
return std::move(o).str();
}
}
Первый параметр функции определяет порядок между этими алгоритмами ( "первый выбор", "второй выбор",..). Чтобы выбрать алгоритм, мы просто отправляем в наилучшее жизнеспособное соответствие:
template<typename T>
auto stringify(T&& t)
-> decltype( detail::stringify(choice<0>{}, std::forward<T>(t)) )
{
return detail::stringify(choice<0>{}, std::forward<T>(t));
}
Как это реализовано? Мы немного крадемся от Xeo @Flaming Dangerzone и Paul @ void_t
"могут реализовывать концепции" ? (с использованием упрощенных реализаций):
constexpr static std::size_t choice_max = 10;
template<std::size_t N> struct choice : choice<N+1>
{
static_assert(N < choice_max, "");
};
template<> struct choice<choice_max> {};
#include <type_traits>
template<typename T, typename = void> struct models : std::false_type {};
template<typename MF, typename... Args>
struct models<MF(Args...),
decltype(MF{}.requires_(std::declval<Args>()...),
void())>
: std::true_type {};
#define REQUIRES(...) std::enable_if_t<models<__VA_ARGS__>::value>* = nullptr
Классы выбора наследуют от худших вариантов: choice<0>
наследует от choice<1>
. Поэтому для аргумента типа choice<0>
параметр функции типа choice<0>
лучше, чем choice<1>
, что лучше, чем choice<2>
и т.д. [Over.ics.rank] p4.4
Обратите внимание, что более специализированный тай-брейкер применяется только в том случае, если ни одна из двух функций не лучше. Из-за общего порядка choice
s мы никогда не столкнемся с этой ситуацией. Это предотвращает неоднозначность вызовов, даже если несколько алгоритмов жизнеспособны.
Мы определяем наши черты типа:
#include <string>
#include <sstream>
namespace helper
{
using std::to_string;
struct has_to_string
{
template<typename T>
auto requires_(T&& t) -> decltype( to_string(std::forward<T>(t)) );
};
struct has_output_operator
{
std::ostream& ostream();
template<typename T>
auto requires_(T&& t) -> decltype(ostream() << std::forward<T>(t));
};
}
Макросов можно избежать, используя идею Р. Мартиньо Фернандеса:
template<typename T>
using requires = std::enable_if_t<models<T>::value, int>;
// exemplary application:
template<typename T, requires<helper::has_to_string(T)> = 0>
std::string stringify(choice<0>, T&& t)
{
using std::to_string;
return to_string(std::forward<T>(t));
}
Ответ 6
Ну, вы можете просто пропустить всю магию метапрограммирования и использовать 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&>(ostringstream() << x).str())
{
return static_cast<ostringstream&>(ostringstream() << x).str();
}
);
Или даже более компактный, если вы не возражаете против макросов:
FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional(
[](auto x) FIT_RETURNS(to_string(x)),
[](auto x) FIT_RETURNS(static_cast<ostringstream&>(ostringstream() << x).str())
);
Обратите внимание, что я также ограничил вторую функцию, поэтому, если тип не может быть вызван с помощью to_string
и не передан в поток ostringstream
, тогда функция не может быть вызвана. Это помогает с улучшенными сообщениями об ошибках и лучшей совместимостью с проверкой требований типа.