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!

Kerrek SB
quelle
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)...};
}
Xeo
quelle
@ 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;
}
Tony Olsson
quelle
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.

AndyG
quelle
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

AndyG
quelle
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)
user5673656
quelle
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!")
CW Holeman II
quelle
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();
}
DarioP
quelle
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 << ")";
}
Gabriel
quelle
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;
}
user2445507
quelle
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.

tahsmith
quelle