Pretty-print std :: tuple


86

Esta é uma continuação da minha pergunta anterior sobre contêineres STL de impressão bonita , para a qual conseguimos desenvolver uma solução muito elegante e totalmente geral.


Nesta próxima etapa, gostaria de incluir a impressão bonita para std::tuple<Args...>, usando modelos variadic (portanto, é estritamente C ++ 11). Pois std::pair<S,T>, eu simplesmente digo

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

Qual é a construção análoga para imprimir uma tupla?

Eu tentei vários pedaços de desempacotamento da pilha de argumentos do template, passando índices e usando SFINAE para descobrir quando estou no último elemento, mas sem sucesso. Não vou sobrecarregá-lo com meu código quebrado; a descrição do problema é esperançosamente direta o suficiente. Essencialmente, gostaria do seguinte comportamento:

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

Pontos de bônus por incluir o mesmo nível de generalidade (char / wchar_t, delimitadores de pares) da pergunta anterior!


Alguém colocou algum código aqui em uma biblioteca? Ou mesmo um .hpp-com-tudo-em que se possa pegar e usar?
einpoklum

@einpoklum: Talvez cxx-prettyprint ? Era para isso que eu precisava daquele código.
Kerrek SB

1
Ótima pergunta, e +1 para "Não vou incomodá-lo com meu código quebrado", embora esteja surpreso que pareça ter realmente conseguido se defender das hordas estúpidas "o que você já tentou".
Don Hatch

Respostas:


78

Oba, í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 << ")";
}

Exemplo ao vivo em Ideone.


Para o delimitador, basta adicionar estas especializações parciais:

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

e altere o operator<<e de print_tupleacordo:

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

E

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

@Kerrek: Atualmente estou testando e me consertando, mas recebo uma saída estranha no Ideone.
Xeo

Acho que você também está confundindo streams e strings. Você está escrevendo algo semelhante a "std :: cout << std :: cout". Em outras palavras, TuplePrinternão tem um operator<<.
Kerrek SB

1
@Thomas: Você não pode usar apenas class Tuplepara operator<<sobrecarregar - seria escolhido para todas e quaisquer coisas. Seria necessária uma restrição, o que meio que implica a necessidade de algum tipo de argumento variável.
Xeo

1
@DanielFrey: Isso é um problema resolvido, garantias lista de inicialização da esquerda para a direita ordem: swallow{(os << get<Is>(t))...};.
Xeo

6
@Xeo Eu peguei emprestado sua andorinha por preferência , se você não se importa.
Cubbi

19

Consegui fazer isso funcionando bem em C ++ 11 (gcc 4.7). Tenho certeza de que existem algumas armadilhas que não considerei, mas acho que o código é fácil de ler e não é complicado. A única coisa que pode ser estranha é a estrutura de "guarda" tuple_printer que garante que encerremos quando o último elemento for atingido. A outra coisa estranha pode ser sizeof ... (Tipos) que retorna o número de tipos no pacote de tipos de Tipos. É usado para determinar o índice do último elemento (tamanho ... (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;
}

1
Sim, parece sensato - talvez com outra especialização para a tupla vazia, para integridade.
Kerrek SB

@KerrekSB, Não existe uma maneira simples de imprimir tuplas em c ++ ?, em python a função retorna implicitamente uma tupla e você pode simplesmente imprimi-los, em c ++ para retornar as variáveis ​​múltiplas de uma função que preciso empacotá-los usando std::make_tuple(). mas na hora de imprimi-lo main(), ele lança um monte de erros !, Alguma sugestão de maneira mais simples de imprimir as tuplas?
Anu

19

No C ++ 17, podemos fazer isso com um pouco menos de código, aproveitando as vantagens das expressões Fold , especialmente uma dobra esquerda unária:

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

Saídas de demonstração ao vivo :

(5, Olá, -0,1)

dado

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

Explicação

Nossa dobra esquerda unária é da forma

... op pack

onde opem nosso cenário é o operador vírgula, e packé a expressão que contém nossa tupla em um contexto não expandido como:

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

Então, se eu tiver uma tupla como esta:

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

E um std::integer_sequencecujos valores são especificados por um modelo sem tipo (veja o código acima)

size_t... I

Então a expressão

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

É expandido para

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

Que vai imprimir

5Olá-0.1

O que é nojento, então precisamos fazer mais alguns truques para adicionar um separador de vírgula a ser impresso primeiro, a menos que seja o primeiro elemento.

Para fazer isso, modificamos a packparte da expressão de dobra para imprimir " ,"se o índice atual Inão for o primeiro, portanto, a (I == 0? "" : ", ")parte * :

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

E agora vamos conseguir

5, Olá, -0,1

O que parece melhor (Nota: eu queria uma saída semelhante a esta resposta )

* Observação: você poderia fazer a separação por vírgula de várias maneiras diferentes das que acabei fazendo. Eu inicialmente adicionei vírgulas condicionalmente depois em vez de antes testando contra std::tuple_size<TupType>::value - 1, mas isso era muito longo, então eu testei contra sizeof...(I) - 1, mas no final eu copiei Xeo e acabamos com o que eu tenho.


1
Você também pode usar if constexprpara o caso base.
Kerrek SB

@KerrekSB: Para decidir se deve imprimir uma vírgula? Não é uma má ideia, gostaria que fosse no ternário.
AndyG

Uma expressão condicional já é uma expressão constante potencial, então o que você tem já é bom :-)
Kerrek SB

17

Estou surpreso que a implementação em cppreference ainda não tenha sido postada aqui, então farei isso para a posteridade. Está oculto no documento por std::tuple_catisso não é fácil de encontrar. Ele usa uma estrutura de guarda como algumas das outras soluções aqui, mas acho que a delas é, em última análise, mais simples e 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

E um teste:

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

Resultado:

(10, Teste, 3,14, Foo, bar, 10, Teste, 3,14, 10)

Demonstração ao vivo


4

Baseado no 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;
}

com saída:

deep tuple: (Hello, 0.1, (1, 2, 3, four, 5.5), Z)

3

Com base no exemplo em The C ++ Programming Language 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;
    }

Resultado:

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

3

Aproveitando o std::apply(C ++ 17), podemos abandonar o std::index_sequencee definir uma única função:

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

Ou, ligeiramente embelezado com a ajuda de um stringstream:

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

1

Outro, semelhante ao de @Tony Olsson, incluindo uma especialização para tupla vazia, conforme sugerido por @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 << ")";
}

0

Eu gosto da resposta de DarioP, mas stringstream usa heap. Isso pode ser evitado:

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

0

Uma coisa que não gosto nas respostas anteriores que usam expressões de dobra é que elas usam sequências de índice ou sinalizadores para manter o controle do primeiro elemento, o que remove muito do benefício de expressões de dobra limpa.

Aqui está um exemplo que não precisa de indexação, mas obtém um resultado semelhante. (Não tão sofisticado quanto alguns dos outros, mas mais podem ser adicionados).

A técnica é usar o que a dobra já oferece: um caso especial para um elemento. Ou seja, uma dobra de elemento apenas se expande para elem[0], então 2 elementos é elem[0] + elem[1], onde +há alguma operação. O que queremos é que um elemento grave apenas aquele elemento no fluxo e, para mais elementos, faça o mesmo, mas junte cada um com uma gravação adicional de ",". Portanto, mapeando isso para a dobra c ++, queremos que cada elemento seja a ação de escrever algum objeto no fluxo. Queremos que nossa +operação seja intercalar duas gravações com uma gravação ",". Portanto, primeiro transforme nossa sequência de tupla em uma sequência de ações de gravação, CommaJoinercomo a chamei, então, para essa ação, adicione um operator+para juntar duas ações da maneira que queremos, adicionando um "," entre:

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

Uma rápida olhada em godbolt sugere que ele compila muito bem também, todas as chamadas de thunks sendo niveladas.

No entanto, isso precisará de uma segunda sobrecarga para lidar com uma tupla vazia.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.