Pretty-print std :: tuple


86

Jest to kontynuacja mojego poprzedniego pytania o ładnie zadrukowane pojemniki STL , dla których udało nam się opracować bardzo eleganckie iw pełni ogólne rozwiązanie.


W następnym kroku chciałbym dołączyć drukowanie ładnie std::tuple<Args...>przy użyciu szablonów wariadycznych (więc jest to ściśle C ++ 11). Po std::pair<S,T>prostu mówię

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

Jaka jest analogiczna konstrukcja do drukowania krotki?

Próbowałem rozpakować różne fragmenty stosu argumentów szablonów, przekazywać indeksy i używać SFINAE, aby dowiedzieć się, kiedy jestem na ostatnim elemencie, ale bezskutecznie. Nie będę cię obciążać moim złamanym kodem; miejmy nadzieję, że opis problemu jest wystarczająco prosty. Zasadniczo chciałbym zachować następujące zachowanie:

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

Dodatkowe punkty za uwzględnienie tego samego poziomu ogólności (char / wchar_t, separatory par) jak w poprzednim pytaniu!


Czy ktoś umieścił tu jakiś kod w bibliotece? A może nawet plik .hpp-ze-wszystkim-w którym można by złapać i używać?
einpoklum

@einpoklum: Może cxx-prettyprint ? Do tego potrzebowałem tego kodu.
Kerrek SB,

1
Świetne pytanie i +1 za „Nie będę cię obciążać moim zepsutym kodem”, chociaż jestem zaskoczony, że rzeczywiście udało mi się odpędzić bezmyślne hordy „czego próbowałeś”.
Don Hatch

Odpowiedzi:


78

Tak, indeksy ~

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

Przykład na żywo w Ideone.


W przypadku ograniczników wystarczy dodać te częściowe specjalizacje:

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

i odpowiednio zmienić operator<<i print_tupleodpowiednio:

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

I

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: Obecnie testuję i naprawiam siebie, ale w Ideone dostaję dziwne wyniki.
Xeo,

Myślę, że mylisz również strumienie i struny. Piszesz coś podobnego do „std :: cout << std :: cout”. Innymi słowy, TuplePrinternie ma rozszerzenia operator<<.
Kerrek SB,

1
@Thomas: Nie możesz po prostu użyć class Tupledo operator<<przeciążenia - zostałby wybrany do wszystkiego. Wymagałoby to ograniczenia, co w pewnym sensie implikuje potrzebę użycia różnego rodzaju argumentów.
Xeo

1
@DanielFrey: To rozwiązuje problem gwarancje lista inicjalizacja lewej do prawej kolejności: swallow{(os << get<Is>(t))...};.
Xeo

6
@Xeo Pożyczyłem twoją jaskółkę dla preferencji , jeśli nie masz nic przeciwko.
Cubbi

20

W C ++ 17 możemy to osiągnąć przy odrobinie mniejszej ilości kodu, wykorzystując wyrażenia fold , w szczególności jednoargumentowe lewe zawinięcie:

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

Wyjścia Live Demo :

(5, Witaj, -0,1)

dany

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

Wyjaśnienie

Nasza jednoargumentowa lewa fałda ma postać

... op pack

gdzie opw naszym scenariuszu jest operatorem przecinka i packjest wyrażeniem zawierającym naszą krotkę w nierozwiniętym kontekście, takim jak:

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

Więc jeśli mam taką krotkę:

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

A std::integer_sequencektórego wartości są określone przez szablon inny niż typ (patrz kod powyżej)

size_t... I

Potem wyrażenie

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

Rozszerza się do

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

Który zostanie wydrukowany

5 Witaj-0.1

Co jest obrzydliwe, więc musimy zrobić więcej sztuczek, aby dodać separator przecinka, który zostanie wydrukowany jako pierwszy, chyba że jest to pierwszy element.

Aby to osiągnąć, modyfikujemy packczęść wyrażenia zwijania, aby była drukowana, " ,"jeśli bieżący indeks Inie jest pierwszym, stąd (I == 0? "" : ", ")część * :

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

A teraz dostaniemy

5, Cześć, -0,1

Co wygląda ładniej (uwaga: chciałem uzyskać podobny wynik jak ta odpowiedź )

* Uwaga: Możesz zrobić separację przecinków na różne sposoby niż to, z czym skończyłem. Początkowo dodałem przecinki warunkowo po, a nie przed , testując przeciwko std::tuple_size<TupType>::value - 1, ale to było za długie, więc zamiast tego testowałem przeciwko sizeof...(I) - 1, ale w końcu skopiowałem Xeo i skończyło się na tym, co mam.


1
Możesz również użyć if constexprdo przypadku podstawowego.
Kerrek SB

@KerrekSB: Za podjęcie decyzji, czy wydrukować przecinek? Niezły pomysł, szkoda, że ​​nie przyszedł w trójek.
AndyG

Wyrażenie warunkowe jest już potencjalnym wyrażeniem stałym, więc to, co masz, jest już dobre :-)
Kerrek SB

19

Mam to działa dobrze w C ++ 11 (gcc 4.7). Są pewne pułapki, których nie rozważałem, ale myślę, że kod jest łatwy do odczytania i nie jest skomplikowany. Jedyną rzeczą, która może być dziwna, jest struktura "guard" tuple_printer, która zapewnia, że ​​zakończymy działanie, gdy osiągnięty zostanie ostatni element. Inną dziwną rzeczą może być sizeof ... (Types), które zwraca liczbę typów w paczce typów. Służy do określenia indeksu ostatniego elementu (rozmiar ... (Typy) - 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
Tak, to wygląda rozsądnie - być może z inną specjalizacją dla pustej krotki, dla kompletności.
Kerrek SB,

@KerrekSB, Nie ma prostego sposobu na wydrukowanie krotek w c ++ ?, w funkcji Pythona niejawnie zwraca krotkę i można je po prostu wydrukować w języku c ++, aby zwrócić wiele zmiennych z funkcji, której potrzebuję, aby je spakować std::make_tuple(). ale w momencie drukowania tego main(), wyrzuca kilka błędów !, Jakieś sugestie dotyczące prostszego sposobu drukowania krotek?
Anu

17

Dziwię się, że implementacja cppreference nie została jeszcze opublikowana, więc zrobię to dla potomności. Jest ukryty w dokumencie, std::tuple_catwięc nie jest łatwo go znaleźć. Używa struktury ochronnej, takiej jak niektóre inne rozwiązania tutaj, ale myślę, że ich rozwiązanie jest ostatecznie prostsze i łatwiejsze do naśladowania.

#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

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

Wynik:

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

Live Demo


4

Oparty na kodzie AndyG, dla 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;
}

z wyjściem:

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

3

Na podstawie przykładu w języku programowania C ++ autorstwa Bjarne Stroustrup, strona 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;
    }

Wynik:

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

3

Wykorzystując std::apply(C ++ 17) możemy porzucić std::index_sequencei zdefiniować jedną funkcję:

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

Lub lekko ozdobione za pomocą struny:

#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

Kolejny, podobny do @Tony'ego Olssona, zawierający specjalizację dla pustej krotki, zgodnie z sugestią @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

Podoba mi się odpowiedź DarioP, ale stringstream używa sterty. Można tego uniknąć:

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

Jedną rzeczą, której nie lubię w poprzednich odpowiedziach, które używają wyrażeń zwinięcia, jest to, że używają sekwencji indeksu lub flag do śledzenia pierwszego elementu, co usuwa wiele korzyści z ładnych, czystych wyrażeń zwiniętych.

Oto przykład, który nie wymaga indeksowania, ale daje podobny wynik. (Nie tak wyrafinowane jak niektóre inne, ale można by dodać więcej).

Technika polega na użyciu tego, co już daje Ci zagięcie: specjalny przypadek na jeden element. To znaczy jeden element zwinięty po prostu się rozwija elem[0], a następnie są 2 elementy elem[0] + elem[1], gdzie +jest jakaś operacja. Chcemy, aby jeden element zapisywał tylko ten element do strumienia, a dla większej liczby elementów robił to samo, ale łączył każdy z dodatkowym zapisem „,”. Więc mapując to na fold c ++, chcemy, aby każdy element był akcją zapisywania jakiegoś obiektu w strumieniu. Chcemy, aby nasza +operacja polegała na przecinaniu dwóch zapisów za pomocą „,”. Więc najpierw przekształć naszą sekwencję krotek w sekwencję akcji zapisu, CommaJoinernazwałem ją, a następnie dla tej akcji dodaj operator+znak, aby połączyć dwie akcje w sposób, w jaki chcemy, dodając „,” pomiędzy:

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

Pobieżne spojrzenie na godbolt sugeruje, że to też dobrze się komponuje, a wszystkie krzyki są spłaszczone.

Będzie to jednak wymagało drugiego przeciążenia, aby poradzić sobie z pustą krotką.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.