В этот ответ Я определяю шаблон на основе типа 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();
}
Дип предлагает это скорее чем 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();
}
Недавно проголосовавший за основную библиотеку 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
к вашему сердцу.
Вы можете проконсультироваться текущий рабочий проект ТС от того, как is_detected
и друзья могут быть реализованы. Это довольно похоже на can_apply
в ответе Якка.
С помощью Уолтер Браун 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 { };
Во-первых, я думаю, что SFINAE обычно следует скрывать от интерфейсов. Это делает интерфейс грязным. Уберите SFINAE от поверхности и используйте диспетчеризацию меток, чтобы определить перегрузку.
Во-вторых, я даже скрываю SFINAE от класса черт. Написание кода «могу я сделать X» достаточно распространено в моем опыте, поэтому я не хочу писать грязный код SFINAE для этого. Поэтому вместо этого я пишу общий can_apply
черта, и есть черта, что SFINAE терпит неудачу, если передал неправильные типы, используя decltype
,
Затем мы кормим СФИАНУ decltype
черта can_apply
и получить тип true / false в зависимости от того, произойдет ли сбой приложения.
Это сводит к минимуму объем работы на черту «могу ли я сделать X» и убирает немного хитрый и хрупкий код SFINAE от повседневной работы.
Я использую C ++ 1z void_t
, Осуществить это самостоятельно легко (внизу этого ответа).
Метафункция, похожая на can_apply
предлагается для стандартизации в C ++ 1z, но он не так стабилен, как 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
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;
который работает в большинстве основных компиляторов C ++ 11.
Обратите внимание, что чем проще template<class...>using void_t=void;
не работает в некоторых старых компиляторах C ++ 11 (в стандарте была неоднозначность).
Вы можете написать вспомогательную черту для этого, используя выражение 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>
Я думаю, что есть две проблемы: 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 а также Павел @ 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
с, мы никогда не попадем в такую ситуацию. Это предотвращает неоднозначность вызовов, даже если несколько алгоритмов являются жизнеспособными.
Мы определяем наши черты типа:
#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));
}
Ну, вы можете просто пропустить всю магию метапрограммирования и использовать fit::conditional
адаптер от Поместиться библиотека:
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
тогда функция не может быть вызвана. Это помогает с лучшими сообщениями об ошибках и лучшей совместимости с проверкой требований к типу.