Pretty-print std :: tupla

86

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!

Kerrek SB
fuente
¿Alguien ha puesto algo del código aquí en una biblioteca? ¿O incluso un .hpp-con-todo-en el que se pueda agarrar y usar?
einpoklum
@einpoklum: ¿Quizás cxx-prettyprint ? Para eso necesitaba ese código.
Kerrek SB
1
Gran pregunta, y +1 para "No te cargaré con mi código roto", aunque me sorprende que parezca haber tenido éxito en defenderse de las hordas insensatas de "¿qué has probado?".
Don Hatch

Respuestas:

78

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 en print_tupleconsecuencia:

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)...};
}
Xeo
fuente
@Kerrek: Actualmente estoy probando y arreglándome, aunque obtengo un resultado extraño en Ideone.
Xeo
Creo que también estás confundiendo streams y strings. Estás escribiendo algo parecido a "std :: cout << std :: cout". En otras palabras, TuplePrinterno tieneoperator<< .
Kerrek SB
1
@Thomas: No se puede usar solo class Tuplepara la operator<<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.
Xeo
1
@DanielFrey: Eso es un problema resuelto, garantías lista de inicialización de izquierda a derecha orden: swallow{(os << get<Is>(t))...};.
Xeo
6
@Xeo Tomé prestado tu golondrina como referencia , si no te importa.
Cubbi
19

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;
}
Tony Olsson
fuente
1
Sí, eso parece sensato, quizás con otra especialización para la tupla vacía, para completar.
Kerrek SB
@KerrekSB, ¿no hay una manera simple de imprimir tuplas en c ++ ?, en python la función devuelve implícitamente una tupla y puedes simplemente imprimirlas, en c ++ para devolver las múltiples variables de una función que necesito para empaquetarlas usando std::make_tuple(). pero al momento de imprimirlo main(), arroja un montón de errores !, ¿Alguna sugerencia sobre una forma más sencilla de imprimir las tuplas?
Anu
19

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 :

(5, hola, -0,1)

dado

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

Explicación

Nuestro pliegue izquierdo unario tiene la forma

... op pack

donde open nuestro escenario es el operador de coma, y packes 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_sequencecuyos 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á

5Hola-0.1

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 packparte de la expresión de pliegue para imprimir " ,"si el índice actual Ino es el primero, de ahí la (I == 0? "" : ", ")parte * :

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

Y ahora tendremos

5, hola, -0.1

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 lugar sizeof...(I) - 1, pero al final copié Xeo y terminamos con lo que tengo.

AndyG
fuente
1
También puede utilizarlo if constexprpara el caso base.
Kerrek SB
@KerrekSB: ¿Para decidir si se debe imprimir una coma? No es mala idea, ojalá viniera en ternario.
AndyG
Una expresión condicional ya es una expresión constante potencial, por lo que lo que tiene ya es bueno :-)
Kerrek SB
17

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_catlo 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:

(10, Prueba, 3.14, Foo, bar, 10, Prueba, 3.14, 10)

Demo en vivo

AndyG
fuente
4

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)
usuario5673656
fuente
3

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!")
CW Holeman II
fuente
3

Aprovechando std::apply(C ++ 17) podemos eliminar std::index_sequencey 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();
}
DarioP
fuente
1

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 << ")";
}
Gabriel
fuente
0

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;
}
usuario2445507
fuente
0

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 es elem[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, la CommaJoinerhe llamado, luego para esa acción agregue una operator+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.

tahsmith
fuente