Esta es una continuación de mi pregunta anterior sobre contenedores STL de impresión bonita , para la cual logramos desarrollar una solución muy elegante y completamente general.
En este siguiente paso, me gustaría incluir la impresión bonita para std::tuple<Args...>
, usando plantillas variadas (por lo que esto es estrictamente C ++ 11). Porque 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?
Probé varios bits de desempaquetado de la pila de argumentos de la plantilla, pasando índices y usando SFINAE para descubrir cuándo estoy en el último elemento, pero sin éxito. No te cargaré con mi código roto; Es de esperar que la descripción del problema sea lo suficientemente sencilla. Básicamente, 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!
fuente
Respuestas:
Sí, índices ~
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 las cosas del delimitador, simplemente agregue 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 enprint_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)...}; }
fuente
TuplePrinter
no tieneoperator<<
.class Tuple
para laoperator<<
sobrecarga, se elegiría para todas y cada una de las cosas. Necesitaría una restricción, lo que implica la necesidad de algún tipo de argumentos variados.swallow{(os << get<Is>(t))...};
.Conseguí que esto funcionara bien en C ++ 11 (gcc 4.7). Estoy seguro de que hay algunas trampas que no he considerado, pero creo que el código es fácil de leer y no es complicado. Lo único que puede resultar extraño es la estructura "guard" tuple_printer que asegura que terminamos cuando se alcanza el último elemento. La otra cosa extraña puede ser sizeof ... (Tipos) que devuelve el número de tipos en el paquete de tipos de tipos. Se utiliza 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; }
fuente
std::make_tuple()
. pero al momento de imprimirlomain()
, arroja un montón de errores !, ¿Alguna sugerencia sobre una forma más sencilla de imprimir las tuplas?En C ++ 17 podemos lograr esto con un poco menos de código aprovechando las expresiones Fold , particularmente un pliegue izquierdo unario:
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)>()); }
Demo en vivoSalidas de :
dado
auto a = std::make_tuple(5, "Hello", -0.1); print(a);
Explicación
Nuestro pliegue izquierdo unario tiene la forma
donde
op
en nuestro escenario es el operador de coma, ypack
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 a
std::integer_sequence
cuyos valores están especificados por una plantilla sin tipo (ver código anterior)size_t... I
Entonces la expresion
(..., (std::cout << std::get<I>(myTuple))
Se expande en
((std::cout << std::get<0>(myTuple)), (std::cout << std::get<1>(myTuple))), (std::cout << std::get<2>(myTuple));
Que imprimirá
Lo cual es asqueroso, por lo que necesitamos hacer algunos trucos más para agregar un separador de coma para que se imprima primero a menos que sea el primer elemento.
Para lograr eso, modificamos la
pack
parte de la expresión de pliegue para imprimir" ,"
si el índice actualI
no es el primero, de ahí la(I == 0? "" : ", ")
parte * :(..., (std::cout << (I == 0? "" : ", ") << std::get<I>(_tup)));
Y ahora tendremos
Que se ve mejor (Nota: quería un resultado similar a esta respuesta )
* Nota: Puede hacer la separación por comas de varias formas distintas a las que obtuve. Inicialmente agregué comas condicionalmente después en lugar de antes probando contra
std::tuple_size<TupType>::value - 1
, pero eso fue demasiado largo, así que probé en su lugarsizeof...(I) - 1
, pero al final copié Xeo y terminamos con lo que tengo.fuente
if constexpr
para el caso base.Me sorprende que la implementación de cppreference no se haya publicado aquí, así que lo haré para la posteridad. Está oculto en el documento, por
std::tuple_cat
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 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:
Demo en vivo
fuente
Basado en código AndyG, para C ++ 17
#include <iostream> #include <tuple> template<class TupType, size_t... I> std::ostream& tuple_print(std::ostream& os, const TupType& _tup, std::index_sequence<I...>) { os << "("; (..., (os << (I == 0 ? "" : ", ") << std::get<I>(_tup))); os << ")"; return os; } template<class... T> std::ostream& operator<< (std::ostream& os, const std::tuple<T...>& _tup) { return tuple_print(os, _tup, std::make_index_sequence<sizeof...(T)>()); } int main() { std::cout << "deep tuple: " << std::make_tuple("Hello", 0.1, std::make_tuple(1,2,3,"four",5.5), 'Z') << std::endl; return 0; }
con salida:
deep tuple: (Hello, 0.1, (1, 2, 3, four, 5.5), Z)
fuente
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!")
fuente
Aprovechando
std::apply
(C ++ 17) podemos eliminarstd::index_sequence
y definir una sola función:#include <tuple> #include <iostream> template<class Ch, class Tr, class... Args> auto& operator<<(std::basic_ostream<Ch, Tr>& os, std::tuple<Args...> const& t) { std::apply([&os](auto&&... args) {((os << args << " "), ...);}, t); return os; }
O, ligeramente adornado con la ayuda de un hilo:
#include <tuple> #include <iostream> #include <sstream> template<class Ch, class Tr, class... Args> auto& operator<<(std::basic_ostream<Ch, Tr>& os, std::tuple<Args...> const& t) { std::basic_stringstream<Ch, Tr> ss; ss << "[ "; std::apply([&ss](auto&&... args) {((ss << args << ", "), ...);}, t); ss.seekp(-2, ss.cur); ss << " ]"; return os << ss.str(); }
fuente
Otro, similar al de @Tony Olsson, que incluye una especialización para la tupla vacía, como 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 << ")"; }
fuente
Me gusta la respuesta de DarioP, pero stringstream usa heap. Esto se puede evitar:
template <class... Args> std::ostream& operator<<(std::ostream& os, std::tuple<Args...> const& t) { os << "("; bool first = true; std::apply([&os, &first](auto&&... args) { auto print = [&] (auto&& val) { if (!first) os << ","; (os << " " << val); first = false; }; (print(args), ...); }, t); os << " )"; return os; }
fuente
Una cosa que no me gusta de las respuestas anteriores que usan expresiones de plegado es que usan secuencias de índice o indicadores para realizar un seguimiento del primer elemento, lo que elimina gran parte del beneficio de las expresiones de plegado limpias y agradables.
Aquí hay un ejemplo que no necesita indexación, pero logra un resultado similar. (No es tan sofisticado como algunos de los otros, pero se podrían agregar más).
La técnica consiste en utilizar lo que ya te da el pliegue: un caso especial para un elemento. Es decir, un elemento plegado simplemente se expande a
elem[0]
, luego 2 elementos eselem[0] + elem[1]
, donde+
hay alguna operación. Lo que queremos es que un elemento escriba solo ese elemento en la secuencia, y para más elementos, haga lo mismo, pero unir cada uno con una escritura adicional de ",". Así que mapeando esto en el pliegue de c ++, queremos que cada elemento sea la acción de escribir algún objeto en la secuencia. Queremos que nuestra+
operación sea intercalar dos escrituras con una escritura ",". Así que primero transforme nuestra secuencia de tuplas en una secuencia de acciones de escritura, laCommaJoiner
he llamado, luego para esa acción agregue unaoperator+
para unir dos acciones de la manera que queramos, agregando un "," en el medio:#include <tuple> #include <iostream> template <typename T> struct CommaJoiner { T thunk; explicit CommaJoiner(const T& t) : thunk(t) {} template <typename S> auto operator+(CommaJoiner<S> const& b) const { auto joinedThunk = [a=this->thunk, b=b.thunk] (std::ostream& os) { a(os); os << ", "; b(os); }; return CommaJoiner<decltype(joinedThunk)>{joinedThunk}; } void operator()(std::ostream& os) const { thunk(os); } }; template <typename ...Ts> std::ostream& operator<<(std::ostream& os, std::tuple<Ts...> tup) { std::apply([&](auto ...ts) { return (... + CommaJoiner{[=](auto&os) {os << ts;}});}, tup)(os); return os; } int main() { auto tup = std::make_tuple(1, 2.0, "Hello"); std::cout << tup << std::endl; }
Una mirada superficial a godbolt sugiere que esto también se compila bastante bien, todas las llamadas de thunks se aplanan.
Sin embargo, esto necesitará una segunda sobrecarga para lidiar con una tupla vacía.
fuente