c++ c++11 tuples variadic-templates

c++ - Pretty-print std:: tuple



c++11 tuples (7)

Este es un seguimiento de mi pregunta anterior sobre los envases STL de impresión bonita , para lo cual logramos desarrollar una solución muy elegante y completamente general.

En este siguiente paso, me gustaría incluir impresión bonita para std::tuple<Args...> , utilizando plantillas variadic (así que esto es estrictamente C ++ 11). Para std::pair<S,T> , simplemente digo

std::ostream & operator<<(std::ostream & o, const std::pair<S,T> & p) { return o << "(" << p.first << ", " << p.second << ")"; }

¿Cuál es la construcción análoga para imprimir una tupla?

He intentado varios bits de pila de argumentos de plantilla desempacando, pasando índices y utilizando SFINAE para descubrir cuándo estoy en el último elemento, pero sin éxito. No te cargaré con mi código roto; la descripción del problema es, con suerte, bastante directa. Esencialmente, me gustaría el siguiente comportamiento:

auto a = std::make_tuple(5, "Hello", -0.1); std::cout << a << std::endl; // prints: (5, "Hello", -0.1)

¡Puntos de bonificación por incluir el mismo nivel de generalidad (char / wchar_t, delimitadores de pares) que la pregunta anterior!


Basado en el ejemplo del lenguaje de programación C ++ de Bjarne Stroustrup, página 817 :

#include <tuple> #include <iostream> #include <string> #include <type_traits> template<size_t N> struct print_tuple{ template<typename... T>static typename std::enable_if<(N<sizeof...(T))>::type print(std::ostream& os, const std::tuple<T...>& t) { char quote = (std::is_convertible<decltype(std::get<N>(t)), std::string>::value) ? ''"'' : 0; os << ", " << quote << std::get<N>(t) << quote; print_tuple<N+1>::print(os,t); } template<typename... T>static typename std::enable_if<!(N<sizeof...(T))>::type print(std::ostream&, const std::tuple<T...>&) { } }; std::ostream& operator<< (std::ostream& os, const std::tuple<>&) { return os << "()"; } template<typename T0, typename ...T> std::ostream& operator<<(std::ostream& os, const std::tuple<T0, T...>& t){ char quote = (std::is_convertible<T0, std::string>::value) ? ''"'' : 0; os << ''('' << quote << std::get<0>(t) << quote; print_tuple<1>::print(os,t); return os << '')''; } int main(){ std::tuple<> a; auto b = std::make_tuple("One meatball"); std::tuple<int,double,std::string> c(1,1.2,"Tail!"); std::cout << a << std::endl; std::cout << b << std::endl; std::cout << c << std::endl; }

Salida:

() ("One meatball") (1, 1.2, "Tail!")


En C ++ 17 podemos lograr esto con un poco menos de código aprovechando las expresiones Fold , particularmente un doblez unario izquierdo:

template<class TupType, size_t... I> void print(const TupType& _tup, std::index_sequence<I...>) { std::cout << "("; (..., (std::cout << (I == 0? "" : ", ") << std::get<I>(_tup))); std::cout << ")/n"; } template<class... T> void print (const std::tuple<T...>& _tup) { print(_tup, std::make_index_sequence<sizeof...(T)>()); }

Resultados de demostración en vivo :

(5, Hola, -0.1)

dado

auto a = std::make_tuple(5, "Hello", -0.1); print(a);

Explicación

Nuestro doblez izquierdo único es de la forma

... op pack

donde op en nuestro escenario es el operador de coma, y pack es la expresión que contiene nuestra tupla en un contexto no expandido como:

(..., (std::cout << std::get<I>(myTuple))

Entonces si tengo una tupla como esta:

auto myTuple = std::make_tuple(5, "Hello", -0.1);

Y una cppreference cuyos valores están especificados por una plantilla sin tipo (ver el código anterior)

size_t... I

Entonces la expresión

(..., (std::cout << std::get<I>(myTuple))

Se expande a

((std::cout << std::get<0>(myTuple)), (std::cout << std::get<1>(myTuple))), (std::cout << std::get<2>(myTuple));

Cuál imprimirá

5Hola-0.1

Que es asqueroso, así que tenemos que hacer un truco más para agregar un separador de comas que se imprimirá primero a menos que sea el primer elemento.

Para lograr eso, modificamos la porción del pack de la expresión fold para imprimir " ," si el índice actual I no es el primero, de ahí la porción (I == 0? "" : ", ") * :

(..., (std::cout << (I == 0? "" : ", ") << std::get<I>(_tup)));

Y ahora conseguiremos

5, hola, -0.1

Que se ve mejor (Nota: quería resultados similares a esta respuesta )

* Nota: Podrías hacer la separación de las comas en una variedad de formas con las que terminé. Inicialmente agregué comas condicionalmente después de en lugar de antes probando contra std::tuple_size<TupType>::value - 1 , pero eso fue demasiado largo, así que probé en cambio contra sizeof...(I) - 1 , pero al final Copié a Xeo y terminamos con lo que tengo.


Me salió bien en C ++ 11 (gcc 4.7). Estoy seguro de algunos escollos que no he considerado, pero creo que el código es fácil de leer y no complicado. Lo único que puede ser extraño es la estructura de "guardia" tuple_printer que asegura que terminamos cuando se alcanza el último elemento. La otra cosa extraña puede ser sizeof ... (Tipos) que devuelve la cantidad de tipos en Tipos tipo paquete. Se usa para determinar el índice del último elemento (tamaño ... (Tipos) - 1).

template<typename Type, unsigned N, unsigned Last> struct tuple_printer { static void print(std::ostream& out, const Type& value) { out << std::get<N>(value) << ", "; tuple_printer<Type, N + 1, Last>::print(out, value); } }; template<typename Type, unsigned N> struct tuple_printer<Type, N, N> { static void print(std::ostream& out, const Type& value) { out << std::get<N>(value); } }; template<typename... Types> std::ostream& operator<<(std::ostream& out, const std::tuple<Types...>& value) { out << "("; tuple_printer<std::tuple<Types...>, 0, sizeof...(Types) - 1>::print(out, value); out << ")"; return out; }


Me sorprende que la implementación en cppreference aún no se haya publicado aquí, así que lo haré para la posteridad. Está escondido en el documento para std::tuple_cat por lo que no es fácil de encontrar. Utiliza una estructura de guardia como algunas de las otras soluciones aquí, pero creo que la suya es en última instancia más simple y más fácil de seguir.

#include <iostream> #include <tuple> #include <string> // helper function to print a tuple of any size template<class Tuple, std::size_t N> struct TuplePrinter { static void print(const Tuple& t) { TuplePrinter<Tuple, N-1>::print(t); std::cout << ", " << std::get<N-1>(t); } }; template<class Tuple> struct TuplePrinter<Tuple, 1> { static void print(const Tuple& t) { std::cout << std::get<0>(t); } }; template<class... Args> void print(const std::tuple<Args...>& t) { std::cout << "("; TuplePrinter<decltype(t), sizeof...(Args)>::print(t); std::cout << ")/n"; } // end helper function

Y una prueba:

int main() { std::tuple<int, std::string, float> t1(10, "Test", 3.14); int n = 7; auto t2 = std::tuple_cat(t1, std::make_pair("Foo", "bar"), t1, std::tie(n)); n = 10; print(t2); }

Salida:

(10, Prueba, 3.14, Foo, barra, 10, Prueba, 3.14, 10)

Demo en vivo


Otro, similar al de @Tony Olsson, que incluye una especialización para la tupla vacía, como lo sugiere @Kerrek SB.

#include <tuple> #include <iostream> template<class Ch, class Tr, size_t I, typename... TS> struct tuple_printer { static void print(std::basic_ostream<Ch,Tr> & out, const std::tuple<TS...> & t) { tuple_printer<Ch, Tr, I-1, TS...>::print(out, t); if (I < sizeof...(TS)) out << ","; out << std::get<I>(t); } }; template<class Ch, class Tr, typename... TS> struct tuple_printer<Ch, Tr, 0, TS...> { static void print(std::basic_ostream<Ch,Tr> & out, const std::tuple<TS...> & t) { out << std::get<0>(t); } }; template<class Ch, class Tr, typename... TS> struct tuple_printer<Ch, Tr, -1, TS...> { static void print(std::basic_ostream<Ch,Tr> & out, const std::tuple<TS...> & t) {} }; template<class Ch, class Tr, typename... TS> std::ostream & operator<<(std::basic_ostream<Ch,Tr> & out, const std::tuple<TS...> & t) { out << "("; tuple_printer<Ch, Tr, sizeof...(TS) - 1, TS...>::print(out, t); return out << ")"; }


Sí, indices ~

namespace aux{ template<std::size_t...> struct seq{}; template<std::size_t N, std::size_t... Is> struct gen_seq : gen_seq<N-1, N-1, Is...>{}; template<std::size_t... Is> struct gen_seq<0, Is...> : seq<Is...>{}; template<class Ch, class Tr, class Tuple, std::size_t... Is> void print_tuple(std::basic_ostream<Ch,Tr>& os, Tuple const& t, seq<Is...>){ using swallow = int[]; (void)swallow{0, (void(os << (Is == 0? "" : ", ") << std::get<Is>(t)), 0)...}; } } // aux:: template<class Ch, class Tr, class... Args> auto operator<<(std::basic_ostream<Ch, Tr>& os, std::tuple<Args...> const& t) -> std::basic_ostream<Ch, Tr>& { os << "("; aux::print_tuple(os, t, aux::gen_seq<sizeof...(Args)>()); return os << ")"; }

Ejemplo en vivo en Ideone.

Para el delimitador, solo agrega estas especializaciones parciales:

// Delimiters for tuple template<class... Args> struct delimiters<std::tuple<Args...>, char> { static const delimiters_values<char> values; }; template<class... Args> const delimiters_values<char> delimiters<std::tuple<Args...>, char>::values = { "(", ", ", ")" }; template<class... Args> struct delimiters<std::tuple<Args...>, wchar_t> { static const delimiters_values<wchar_t> values; }; template<class... Args> const delimiters_values<wchar_t> delimiters<std::tuple<Args...>, wchar_t>::values = { L"(", L", ", L")" };

y cambie el operator<< y print_tuple consecuencia:

template<class Ch, class Tr, class... Args> auto operator<<(std::basic_ostream<Ch, Tr>& os, std::tuple<Args...> const& t) -> std::basic_ostream<Ch, Tr>& { typedef std::tuple<Args...> tuple_t; if(delimiters<tuple_t, Ch>::values.prefix != 0) os << delimiters<tuple_t,char>::values.prefix; print_tuple(os, t, aux::gen_seq<sizeof...(Args)>()); if(delimiters<tuple_t, Ch>::values.postfix != 0) os << delimiters<tuple_t,char>::values.postfix; return os; }

Y

template<class Ch, class Tr, class Tuple, std::size_t... Is> void print_tuple(std::basic_ostream<Ch, Tr>& os, Tuple const& t, seq<Is...>){ using swallow = int[]; char const* delim = delimiters<Tuple, Ch>::values.delimiter; if(!delim) delim = ""; (void)swallow{0, (void(os << (Is == 0? "" : delim) << std::get<Is>(t)), 0)...}; }