Pretty print std :: tuple


89

Questo è un follow-up alla mia precedente domanda sui contenitori STL graziosi , per i quali siamo riusciti a sviluppare una soluzione molto elegante e del tutto generale.


In questo passaggio successivo, vorrei includere la stampa graziosa per std::tuple<Args...>, utilizzando modelli variadici (quindi questo è rigorosamente C ++ 11). Perché std::pair<S,T>, dico semplicemente

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

Qual è la costruzione analoga per la stampa di una tupla?

Ho provato vari bit di decompressione dello stack di argomenti del modello, passando gli indici e usando SFINAE per scoprire quando sono all'ultimo elemento, ma senza successo. Non ti appesantirò con il mio codice rotto; si spera che la descrizione del problema sia abbastanza semplice. Essenzialmente, vorrei il seguente comportamento:

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

Punti bonus per aver incluso lo stesso livello di generalità (char / wchar_t, delimitatori di coppia) della domanda precedente!


Qualcuno ha inserito il codice qui in una libreria? O anche un .hpp-con-tutto-in cui si potrebbe afferrare e utilizzare?
einpoklum

@ einpoklum: Forse cxx-prettyprint ? È per questo che avevo bisogno di quel codice.
Kerrek SB

1
Ottima domanda, e +1 per "Non ti appesantirò con il mio codice non funzionante", anche se sono sorpreso che sembra che sia effettivamente riuscito a respingere le orde insensate di "cosa hai provato".
Don Hatch

Risposte:


78

Sì, indici ~

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

Esempio dal vivo su Ideone.


Per il delimitatore, aggiungi semplicemente queste specializzazioni parziali:

// 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 cambia il file operator<<print_tuple conseguenza e :

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: Attualmente sto testando e sistemando me stesso, però ottengo un output strano su Ideone.
Xeo

Penso che tu stia anche confondendo stream e stringhe. Stai scrivendo qualcosa di simile a "std :: cout << std :: cout". In altre parole, TuplePrinternon ha un file operator<<.
Kerrek SB

1
@ Thomas: Non puoi usarlo solo class Tupleper il operator<<sovraccarico: verrebbe scelto per qualsiasi cosa. Avrebbe bisogno di un vincolo, che in qualche modo implica la necessità di un qualche tipo di argomenti variadici.
Xeo

1
@DanielFrey: Questo è un problema risolto, garanzie list-inizializzazione da sinistra a destra ordine: swallow{(os << get<Is>(t))...};.
Xeo

6
@Xeo ho preso in prestito la tua rondine come riferimento , se non ti dispiace.
Cubbi

23

In C ++ 17 possiamo ottenere questo risultato con un po 'meno di codice sfruttando le espressioni Fold , in particolare una piega sinistra unaria:

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

Uscite demo live :

(5, Ciao, -0,1)

dato

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

Spiegazione

La nostra piega sinistra unaria è della forma

... op pack

dove opnel nostro scenario è l'operatore virgola ed packè l'espressione contenente la nostra tupla in un contesto non espanso come:

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

Quindi, se ho una tupla in questo modo:

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

E un i std::integer_sequencecui valori sono specificati da un modello non di tipo (vedi codice sopra)

size_t... I

Poi l'espressione

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

Viene espanso in

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

Che stamperà

5 Hello-0.1

Il che è grossolano, quindi dobbiamo fare qualche trucco per aggiungere un separatore virgola da stampare per primo a meno che non sia il primo elemento.

Per fare ciò, modifichiamo la packparte dell'espressione di piegatura da stampare " ,"se l'indice corrente Inon è il primo, da cui la (I == 0? "" : ", ")parte * :

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

E ora arriveremo

5, ciao, -0.1

Che sembra più bello (Nota: volevo un output simile a questa risposta )

* Nota: puoi eseguire la separazione delle virgole in vari modi rispetto a quello che ho ottenuto io. Inizialmente ho aggiunto le virgole in modo condizionale dopo anziché prima testando contro std::tuple_size<TupType>::value - 1, ma era troppo lungo, quindi ho provato invece contro sizeof...(I) - 1, ma alla fine ho copiato Xeo e abbiamo finito con quello che ho.


1
Puoi anche usarlo if constexprper il case base.
Kerrek SB

@ KerrekSB: Per decidere se stampare una virgola? Non è una cattiva idea, vorrei che fosse ternaria.
AndyG

Un'espressione condizionale è già una potenziale espressione costante, quindi quello che hai è già buono :-)
Kerrek SB

19

Ho funzionato bene in C ++ 11 (gcc 4.7). Ci sono sicuramente alcune insidie ​​che non ho considerato ma penso che il codice sia facile da leggere e non complicato. L'unica cosa che può essere strana è la struttura "guard" tuple_printer che assicura che terminiamo quando viene raggiunto l'ultimo elemento. L'altra cosa strana potrebbe essere sizeof ... (Types) che restituisce il numero di tipi nel type pack dei tipi. Viene utilizzato per determinare l'indice dell'ultimo elemento (dimensione ... (Tipi) - 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
Sì, sembra ragionevole, forse con un'altra specializzazione per la tupla vuota, per la completezza.
Kerrek SB

@ KerrekSB, non esiste un modo semplice per stampare le tuple in c ++ ?, nella funzione python restituisce implicitamente una tupla e puoi semplicemente stamparle, in c ++ per restituire le variabili multiple da una funzione che ho bisogno di comprimerle usando std::make_tuple(). ma al momento della stampa main(), genera un sacco di errori !, Qualche suggerimento su un modo più semplice per stampare le tuple?
Anu

17

Sono sorpreso che l'implementazione su cppreference non sia già stata pubblicata qui, quindi lo farò per i posteri. È nascosto nel documento, std::tuple_catquindi non è facile da trovare. Usa una struttura di guardia come alcune delle altre soluzioni qui, ma penso che la loro sia in definitiva più semplice e facile da seguire.

#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 un test:

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

Produzione:

(10, Test, 3.14, Foo, bar, 10, Test, 3.14, 10)

Dimostrazione dal vivo


4

Basato sul codice AndyG, per 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 uscita:

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

3

Basato sull'esempio del linguaggio di programmazione C ++ di Bjarne Stroustrup, pagina 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;
    }

Produzione:

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

3

Facendo leva su std::apply(C ++ 17) possiamo eliminare std::index_sequencee definire una singola funzione:

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

Oppure, leggermente abbellito con l'aiuto di uno 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

Un altro, simile a quello di @Tony Olsson, inclusa una specializzazione per la tupla vuota, come suggerito da @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

Mi piace la risposta di DarioP, ma stringstream usa heap. Questo può essere evitato:

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

Una cosa che non mi piace delle risposte precedenti che usano espressioni di piegatura è che usano sequenze di indice o flag per tenere traccia del primo elemento, il che rimuove gran parte del vantaggio di belle espressioni di piegatura pulita.

Ecco un esempio che non necessita di indicizzazione, ma ottiene un risultato simile. (Non sofisticato come alcuni degli altri, ma potrebbe essere aggiunto altro.)

La tecnica consiste nell'usare ciò che la piega già ti dà: un caso speciale per un elemento. Cioè, una piega di un elemento si espande a elem[0], quindi 2 elementi sono elem[0] + elem[1], dove +è qualche operazione. Quello che vogliamo è che un elemento scriva solo quell'elemento nello stream e, per più elementi, faccia lo stesso, ma unisca ciascuno di essi con un'ulteriore scrittura di ",". Quindi, mappando questo al c ++ fold, vogliamo che ogni elemento sia l'azione di scrivere un oggetto nello stream. Vogliamo che la nostra +operazione sia quella di intervallare due scritture con una "," scrittura. Quindi prima trasforma la nostra sequenza di tupla in una sequenza di azioni di scrittura, l' CommaJoinerho chiamata, quindi per quell'azione aggiungi un operator+per unire due azioni nel modo che vogliamo, aggiungendo un "," in mezzo:

#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 rapida occhiata a Godbolt suggerisce che anche questo si compila abbastanza bene, tutte le chiamate thunk vengono appiattite.

Tuttavia, sarà necessario un secondo overload per gestire una tupla vuota.


0

Ecco un po 'di codice che ho creato di recente per stampare una tupla.

#include <iostream>
#include <tuple>

using namespace std;

template<typename... Ts>
ostream& operator<<(ostream& output, const tuple<Ts...> t) {
    output << '(';
    apply([&](auto&&... args) {
        ((cout << args << ", "), ...);
    }, t);
    output << "\b\b";
    output << ')';
    return output;
}

Utilizzando il tuo caso di esempio:

auto a = std::make_tuple(5, "Hello", -0.1); 
cout << a << '\n'; // (5, Hello, -0.1)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.