Std :: tuple mit hübschem Druck


85

Dies ist eine Fortsetzung meiner vorherigen Frage zu hübsch gedruckten STL-Containern , für die wir eine sehr elegante und vollständig allgemeine Lösung entwickelt haben.


In diesem nächsten Schritt möchte ich das hübsche Drucken std::tuple<Args...>mit variadischen Vorlagen einschließen (dies ist also ausschließlich C ++ 11). Denn std::pair<S,T>ich sage einfach

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

Was ist die analoge Konstruktion zum Drucken eines Tupels?

Ich habe versucht, verschiedene Teile des Vorlagenargumentstapels zu entpacken, Indizes weiterzugeben und mithilfe von SFINAE herauszufinden, wann ich am letzten Element bin, aber ohne Erfolg. Ich werde dich nicht mit meinem kaputten Code belasten. Die Problembeschreibung ist hoffentlich einfach genug. Im Wesentlichen möchte ich folgendes Verhalten:

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

Bonuspunkte für die Aufnahme der gleichen Allgemeinheit (char / wchar_t, Paarbegrenzer) wie in der vorherigen Frage!


Hat jemand den Code hier in eine Bibliothek gestellt? Oder sogar eine .hpp-mit-allem-in, die man greifen und verwenden könnte?
Einpoklum

@einpoklum: Vielleicht cxx-Prettyprint ? Dafür brauchte ich diesen Code.
Kerrek SB

1
Tolle Frage und +1 für "Ich werde dich nicht mit meinem kaputten Code belasten", obwohl ich überrascht bin, dass es tatsächlich gelungen zu sein scheint, die gedankenlosen "Was hast du versucht?" - Horden abzuwehren.
Don Hatch

Antworten:


78

Ja, Indizes ~

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

Live-Beispiel auf Ideone.


Fügen Sie für das Trennzeichen einfach diese Teilspezialisierungen hinzu:

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

und ändern Sie das operator<<und print_tupleentsprechend:

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

Und

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: Ich teste und repariere mich gerade selbst, aber ich bekomme seltsame Ausgaben auf Ideone.
Xeo

Ich denke, Sie verwirren auch Streams und Strings. Sie schreiben etwas, das "std :: cout << std :: cout" ähnelt. Mit anderen Worten, TuplePrinterhat keine operator<<.
Kerrek SB

1
@Thomas: Sie können nicht nur class Tuplefür die operator<<Überlastung verwenden - es würde für alle Dinge ausgewählt werden. Es würde eine Einschränkung erfordern, die irgendwie die Notwendigkeit einer Art variadischer Argumente impliziert.
Xeo

1
@ DanielFrey: Das ist ein gelöstes Problem. Die Listeninitialisierung garantiert die Reihenfolge von links nach rechts : swallow{(os << get<Is>(t))...};.
Xeo

6
@Xeo Ich habe mir deine Schwalbe zur Referenz geliehen , wenn es dir nichts ausmacht.
Cubbi

19

Ich habe dies in C ++ 11 (gcc 4.7) gut funktioniert. Ich bin mir sicher, dass ich einige Fallstricke nicht berücksichtigt habe, aber ich denke, der Code ist einfach zu lesen und nicht kompliziert. Das einzige, was seltsam sein kann, ist die "guard" -Struktur tuple_printer, die sicherstellt, dass wir beenden, wenn das letzte Element erreicht ist. Die andere seltsame Sache kann sizeof ... (Typen) sein, die die Anzahl der Typen im Typentyppaket zurückgeben. Es wird verwendet, um den Index des letzten Elements zu bestimmen (Größe ... (Typen) - 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
Ja, das sieht vernünftig aus - vielleicht mit einer anderen Spezialisierung für das leere Tupel, der Vollständigkeit halber.
Kerrek SB

@KerrekSB, Es gibt keine einfache Möglichkeit, Tupel in C ++ zu drucken. In der Python-Funktion wird implizit ein Tupel zurückgegeben, und Sie können sie einfach in C ++ drucken, um die mehreren Variablen einer Funktion zurückzugeben, mit der ich sie packen muss std::make_tuple(). Aber zum Zeitpunkt des Druckens main()gibt es eine Reihe von Fehlern! Gibt es Vorschläge für eine einfachere Art, die Tupel zu drucken?
Anu

19

In C ++ 17 können wir dies mit etwas weniger Code erreichen, indem wir Fold-Ausdrücke nutzen , insbesondere eine unäre linke Falte:

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

Live-Demo- Ausgaben:

(5, Hallo, -0,1)

gegeben

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

Erläuterung

Unsere unäre linke Falte hat die Form

... op pack

Dabei ist opin unserem Szenario der Kommaoperator und packder Ausdruck, der unser Tupel enthält, in einem nicht erweiterten Kontext wie:

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

Also, wenn ich so ein Tupel habe:

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

Und a, std::integer_sequencederen Werte durch eine Nicht-Typ-Vorlage angegeben werden (siehe obigen Code)

size_t... I

Dann der Ausdruck

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

Wird erweitert in

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

Welches wird gedruckt

5Hallo-0,1

Was grob ist, also müssen wir noch einige Tricks machen, um ein Komma-Trennzeichen hinzuzufügen, das zuerst gedruckt werden soll, es sei denn, es ist das erste Element.

Um dies zu erreichen, ändern wir den zu druckenden packTeil des Falzausdrucks, " ,"wenn der aktuelle Index Inicht der erste ist, daher der (I == 0? "" : ", ")Teil * :

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

Und jetzt werden wir bekommen

5, Hallo, -0.1

Was schöner aussieht (Hinweis: Ich wollte eine ähnliche Ausgabe wie diese Antwort )

* Hinweis: Sie können die Komma-Trennung auf verschiedene Arten durchführen, als ich es am Ende getan habe. Ich zunächst Komma bedingt hinzugefügt , nachdem statt , bevor durch die Prüfung gegen std::tuple_size<TupType>::value - 1, aber das war zu lang, so dass ich getestet , anstatt gegen sizeof...(I) - 1, aber am Ende habe ich kopiert Xeo und wir am Ende mit dem, was ich habe.


1
Sie können auch if constexprfür den Basisfall verwenden.
Kerrek SB

@ KerrekSB: Um zu entscheiden, ob ein Komma gedruckt werden soll? Keine schlechte Idee, wünschte, es wäre ternär.
AndyG

Ein bedingter Ausdruck ist bereits ein potentieller konstanter Ausdruck, also ist das, was Sie haben, bereits gut :-)
Kerrek SB

17

Ich bin überrascht, dass die Implementierung von cppreference noch nicht hier veröffentlicht wurde, also werde ich es für die Nachwelt tun. Es ist im Dokument versteckt, std::tuple_catdaher ist es nicht leicht zu finden. Es verwendet eine Schutzstruktur wie einige der anderen Lösungen hier, aber ich denke, ihre ist letztendlich einfacher und leichter zu befolgen.

#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

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

Ausgabe:

(10, Test, 3,14, Foo, Balken, 10, Test, 3,14, 10)

Live-Demo


4

Basierend auf AndyG-Code für 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;
}

mit Ausgabe:

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

3

Basierend auf einem Beispiel für die C ++ - Programmiersprache von Bjarne Stroustrup, Seite 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;
    }

Ausgabe:

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

3

Durch die Nutzung von std::apply(C ++ 17) können wir die löschen std::index_sequenceund eine einzelne Funktion definieren:

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

Oder mit Hilfe eines Stringstreams leicht verschönert:

#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

Eine andere, ähnlich wie bei @Tony Olsson, einschließlich einer Spezialisierung für das leere Tupel, wie von @Kerrek SB vorgeschlagen.

#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

Ich mag die Antwort von DarioP, aber Stringstream verwendet Heap. Dies kann vermieden werden:

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

Eine Sache, die ich an den vorherigen Antworten, die Fold-Ausdrücke verwenden, nicht mag, ist, dass sie Indexsequenzen oder Flags verwenden, um das erste Element zu verfolgen, was den Vorteil netter sauberer Fold-Ausdrücke weitgehend beseitigt.

Hier ist ein Beispiel, das keine Indizierung benötigt, aber ein ähnliches Ergebnis erzielt. (Nicht so raffiniert wie einige der anderen, aber es könnten weitere hinzugefügt werden.)

Die Technik besteht darin, das zu verwenden, was die Falte Ihnen bereits bietet: einen Sonderfall für ein Element. Das heißt, eine Elementfalte wird nur erweitert elem[0], dann sind 2 Elemente elem[0] + elem[1], wo +eine Operation ausgeführt wird. Was wir wollen, ist, dass ein Element genau dieses Element in den Stream schreibt, und für mehr Elemente dasselbe tun, aber jedes mit einem zusätzlichen Schreibvorgang von "," verbinden. Wenn wir dies also der c ++ - Falte zuordnen, möchten wir, dass jedes Element die Aktion ist, ein Objekt in den Stream zu schreiben. Wir möchten, dass unsere +Operation darin besteht, zwei Schreibvorgänge mit einem "," - Schreibzugriff zu versehen. Verwandeln Sie also zuerst unsere Tupelsequenz in eine Sequenz von Schreibaktionen, die CommaJoinerich genannt habe, und fügen Sie dann für diese Aktion eine hinzu operator+, um zwei Aktionen auf die von uns gewünschte Weise zu verbinden, und fügen Sie dazwischen ein "," hinzu:

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

Ein flüchtiger Blick auf Godbolt deutet darauf hin, dass dies auch recht gut kompiliert werden kann, da alle Thunks-Anrufe abgeflacht sind.

Dies erfordert jedoch eine zweite Überlastung, um mit einem leeren Tupel fertig zu werden.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.