valores - todas las funciones de c++




Metaprogramación: la falla de la definición de la función define una función separada (4)

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.

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 de ostringstream .

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();
}

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

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.





result-of