c++ - una - tipos de funciones en lenguaje c
Metaprogramación: la falla de la definición de la función define una función separada (6)
En
esta respuesta
, defino una plantilla basada en la propiedad
is_arithmetic
del tipo:
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 sugiere
que, en lugar de la propiedad
is_arithmetic
del tipo, si
to_string
se define para el tipo sea el criterio de selección de plantilla.
Esto es claramente deseable, pero no sé una manera de decir:
Si
std::to_string
no está definido, utilice la sobrecarga deostringstream
.
Declarar los criterios
to_string
es simple:
template<typename T> decltype(to_string(T{})) stringify(T t){
return to_string(t);
}
Es lo opuesto a ese criterio que no puedo entender cómo construir. Obviamente, esto no funciona, pero espero que transmita lo que estoy tratando de construir:
template<typename T> enable_if_t<!decltype(to_string(T{})::value, string> (T t){
return static_cast<ostringstream&>(ostringstream() << t).str();
}
Bueno, puede omitir toda la magia de metaprogramación y usar el adaptador
fit::conditional
de la biblioteca
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();
}
);
O incluso más compacto, si no te importan las macros:
FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional(
[](auto x) FIT_RETURNS(to_string(x)),
[](auto x) FIT_RETURNS(static_cast<ostringstream&>(ostringstream() << x).str())
);
Tenga en cuenta que también restringí la segunda función, por lo que si el tipo no se puede llamar con
to_string
ni transmitirlo a
ostringstream
entonces la función no se puede llamar.
Esto ayuda con mejores mensajes de error y una mejor compatibilidad con la verificación de los requisitos de tipo.
Creo que hay dos problemas: 1) Encontrar todos los algoritmos viables para un tipo dado. 2) Seleccione el mejor.
Podemos, por ejemplo, especificar manualmente un orden para un conjunto de algoritmos sobrecargados:
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();
}
}
El primer parámetro de función especifica el orden entre esos algoritmos ("primera opción", "segunda opción", ..). Para seleccionar un algoritmo, simplemente enviamos a la mejor coincidencia viable:
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));
}
¿Cómo se implementa esto?
¿Robamos un poco de
Xeo @ Flaming Dangerzone
y
Paul @
void_t
"puede implementar conceptos"?
(usando implementaciones simplificadas):
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
Las clases de elección heredan de las peores opciones: la
choice<0>
hereda de la
choice<1>
.
Por lo tanto, para un argumento de tipo
choice<0>
, un parámetro de función de tipo
choice<0>
es una mejor coincidencia que la
choice<1>
, que es una mejor coincidencia que la
choice<2>
y así sucesivamente [over.ics.rank ] p4.4
Tenga en cuenta que el desempate
más especializado se
aplica solo si ninguna de las dos funciones es mejor.
Debido al orden total de
choice
, nunca llegaremos a esa situación.
Esto evita que las llamadas sean ambiguas, incluso si varios algoritmos son viables.
Definimos nuestros rasgos de tipo:
#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));
};
}
Las macros se pueden evitar utilizando una idea de R. Martinho Fernandes :
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));
}
Podría escribir un rasgo auxiliar para esto usando la expresión 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));
Luego use
std::enable_if_t<has_to_string<T>::value>
Primero, creo que SFINAE generalmente debería estar oculto de las interfaces. Hace que la interfaz sea desordenada. Coloque el SFINAE lejos de la superficie y use el envío de etiquetas para elegir una sobrecarga.
En segundo lugar, incluso oculto SFINAE de la clase de rasgos.
Escribir el código "puedo hacer X" es lo suficientemente común en mi experiencia que no quiero tener que escribir código SFINAE desordenado para hacerlo.
Entonces, en cambio, escribo un rasgo genérico de
can_apply
, y tengo un rasgo que SFINAE falla si pasa los tipos incorrectos usando
decltype
.
Luego alimentamos el rasgo de decltype fallido de
decltype
a
can_apply
, y obtenemos un tipo verdadero / falso dependiendo de si la aplicación falla.
Esto reduce el trabajo por rasgo de "puedo hacer X" a una cantidad mínima, y coloca el código SFINAE algo complicado y frágil lejos del trabajo diario.
Yo uso C ++ 1z''s
void_t
.
Implementarlo usted mismo es fácil (al final de esta respuesta).
Se propone una metafunción similar a
can_apply
para la estandarización en C ++ 1z, pero no es tan estable como
void_t
, por lo que no la estoy usando.
Primero, un espacio de nombres de
details
para ocultar que la implementación de
can_apply
se encuentre por accidente:
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{};
}
Luego podemos escribir
can_apply
en términos de
details::can_apply
, y tiene una interfaz más agradable (no requiere que se pase el
void
adicional):
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z, void, Ts...>;
Lo anterior es un código genérico de metaprogramación auxiliar.
Una vez que lo tengamos en su lugar, podemos escribir una clase de rasgos
can_to_string
muy limpiamente:
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 >;
y tenemos un rasgo
can_to_string<T>
que es cierto si podemos
to_string
una
T
El trabajo requiere escribir un nuevo rasgo como ese que ahora es de 2 a 4 líneas de código simple: simplemente haga un tipo de
decltype
using
alias y luego haga una prueba de
can_apply
en él.
Una vez que tenemos eso, usamos el envío de etiquetas para la implementación adecuada:
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>{});
}
Todo el código feo se esconde en el espacio de nombres de
details
.
Si necesita un
void_t
, use esto:
template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;
que funciona en la mayoría de los principales compiladores de C ++ 11.
Tenga en cuenta que la
template<class...>using void_t=void;
más simple
template<class...>using void_t=void;
no funciona en algunos compiladores de C ++ 11 anteriores (había una ambigüedad en el estándar).
Recién votado en los fundamentos de biblioteca TS en la reunión del comité de la semana pasada:
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>;
Luego etiquete despacho y / o SFINAE en
has_to_string
al contenido de su corazón.
Puede consultar
el borrador de trabajo actual del TS
sobre cómo se puede
is_detected
y amigos.
Es bastante similar a
can_apply
en la respuesta de @ Yakk.
Usando
void_t
Walter Brown
:
template <typename...>
using void_t = void;
Es muy fácil hacer un rasgo de este tipo:
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 { };