Довольно печать std :: tuple

86

Это продолжение моего предыдущего вопроса о хорошо печатаемых контейнерах STL , для которого нам удалось разработать очень элегантное и полностью общее решение.


На этом следующем шаге я хотел бы включить красивую печать std::tuple<Args...>с использованием вариативных шаблонов (так что это строго C ++ 11). Для std::pair<S,T>, я просто говорю

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

Какая аналогичная конструкция используется для печати кортежа?

Я пробовал различные части распаковки стека аргументов шаблона, передачи индексов и использования SFINAE, чтобы определить, когда я нахожусь на последнем элементе, но безуспешно. Я не буду обременять вас своим сломанным кодом; Описание проблемы, надеюсь, достаточно прямолинейно. По сути, мне бы хотелось следующего поведения:

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

Бонусные баллы за включение такого же уровня универсальности (char / wchar_t, разделители пар), что и в предыдущем вопросе!

Керрек С.Б.
источник
Кто-нибудь поместил здесь какой-либо код в библиотеку? Или даже .hpp-with-everything-in, который можно было взять и использовать?
einpoklum
@einpoklum: Может, cxx-prettyprint ? Вот для чего мне нужен был этот код.
Kerrek SB
1
Отличный вопрос и +1 за «Я не буду обременять вас своим взломанным кодом», хотя я удивлен, что он, кажется, действительно преуспел в отражении бессмысленных орды «что вы пробовали».
Дон Хэтч

Ответы:

78

Ура, индексы ~

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

Живой пример на Идеоне.


Для разделителя просто добавьте эти частичные специализации:

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

и соответственно измените operator<<и print_tuple:

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

А также

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
источник
@Kerrek: Сейчас я тестирую и чиню себя, но на Ideone получаются странные результаты.
Xeo 05
Я думаю, вы также путаете потоки и строки. Вы пишете что-то вроде "std :: cout << std :: cout". Другими словами, у TuplePrinterнего нет operator<<.
Kerrek SB 05
1
@ Томас: Ты не можешь просто использовать class Tuple для operator<<перегрузки - его выбирают для чего . Для этого потребуется ограничение, которое как бы подразумевает необходимость в каких-то вариативных аргументах.
Xeo
1
@DanielFrey: Это решенная проблема, инициализация списка гарантирует порядок слева направо: swallow{(os << get<Is>(t))...}; .
Xeo
6
@Xeo Я позаимствовал вашу ласточку для справки , если вы не против.
Кубби
20

В C ++ 17 мы можем добиться этого с немного меньшим количеством кода, воспользовавшись выражениями Fold , особенно унарной левой складкой:

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 :

(5, привет, -0,1)

дано

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

Объяснение

Наша унарная левая складка имеет вид

... op pack

где op в нашем сценарии - это оператор запятой, а packэто выражение, содержащее наш кортеж в нерасширенном контексте, например:

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

Итак, если у меня есть такой кортеж:

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

И std::integer_sequence , значения которого указаны не типовым шаблоном (см. Код выше)

size_t... I

Тогда выражение

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

Расширяется до

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

Что напечатает

5Привет-0.1

Это грубо, поэтому нам нужно проделать еще несколько уловок, чтобы добавить разделитель запятой, который будет напечатан первым, если это не первый элемент.

Для этого мы изменяем packчасть выражения свертки для печати, " ,"если текущий индекс Iне является первым, следовательно, (I == 0? "" : ", ")часть * :

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

А теперь получим

5, привет, -0,1

Что выглядит лучше (Примечание: я хотел получить аналогичный результат, как в этом ответе )

* Примечание. Вы можете разделить запятую разными способами, чем я. Сначала я добавил запятые условно после, а не до , проверяя std::tuple_size<TupType>::value - 1, но это было слишком долго, поэтому я протестировал вместо этого sizeof...(I) - 1, но в конце я скопировал Xeo, и мы получили то, что у меня есть.

AndyG
источник
1
Вы также можете использовать if constexprдля базового случая.
Kerrek SB
@KerrekSB: Чтобы решить, печатать ли запятую? Неплохая идея, хотелось бы, чтобы она пришла троично.
AndyG
Условное выражение уже является потенциальным постоянным выражением, так что то, что у вас есть, уже хорошо :-)
Керрек С.Б.
19

У меня это нормально работает на С ++ 11 (gcc 4.7). Я уверен, что есть некоторые подводные камни, которые я не учел, но я думаю, что код легко читается и не сложен. Единственное, что может показаться странным, - это «охранная» структура tuple_printer, которая гарантирует, что мы завершим работу, когда будет достигнут последний элемент. Другая странная вещь может быть sizeof ... (Типы), которые возвращают количество типов в пакете типов типов. Используется для определения индекса последнего элемента (размер ... (Типы) - 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
Да, это выглядит разумным - возможно, с другой специализацией для пустого кортежа для полноты.
Kerrek SB 04
@KerrekSB, Нет простого способа распечатать кортежи в c ++ ?, в функции python неявно возвращает кортеж, и вы можете просто распечатать их в c ++, чтобы вернуть несколько переменных из функции, которую мне нужно упаковать, используя std::make_tuple(). но во время печати он main()выдает кучу ошибок !, Есть ли предложения по более простому способу печати кортежей?
Ану
17

Я удивлен, что реализация cppreference еще не была опубликована здесь, поэтому сделаю это для потомков. Он спрятан в документе, std::tuple_catпоэтому его нелегко найти. Он использует защитную структуру, как и некоторые другие решения здесь, но я думаю, что их решение в конечном итоге проще и понятнее.

#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

И тест:

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

Выход:

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

Живая демонстрация

AndyG
источник
4

На основе кода AndyG для 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;
}

с выходом:

deep tuple: (Hello, 0.1, (1, 2, 3, four, 5.5), Z)
user5673656
источник
3

На основе примера на языке программирования C ++ Бьярна Страуструпа, стр. 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;
    }

Выход:

()
("One meatball")
(1, 1.2, "Tail!")
CW Holeman II
источник
3

Используя std::apply(C ++ 17), мы можем отбросить std::index_sequenceи определить одну функцию:

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

Или, слегка украсив струной:

#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
источник
1

Еще один, похожий на @Tony Olsson, включая специализацию для пустого кортежа, предложенную @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

Мне нравится ответ DarioP, но stringstream использует кучу. Этого можно избежать:

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
источник
0

Что мне не нравится в предыдущих ответах, в которых используются выражения свертки, так это то, что они используют последовательности индексов или флаги для отслеживания первого элемента, что лишает многих преимуществ красивых чистых выражений свертки.

Вот пример, который не требует индексации, но дает аналогичный результат. (Не такой изощренный, как некоторые другие, но можно добавить и другие.)

Техника заключается в использовании того, что уже дает вам складка: особый случай для одного элемента. То есть, один элемент свёртка просто расширяется elem[0], затем два элемента elem[0] + elem[1], где +какая-то операция. Мы хотим, чтобы один элемент записывал только этот элемент в поток, а для большего количества элементов делали то же самое, но присоединялись к каждому с дополнительной записью «,». Итак, отображая это на свертке c ++, мы хотим, чтобы каждый элемент был действием записи некоторого объекта в поток. Мы хотим, чтобы наша +операция заключалась в чередовании двух операций записи с записью «,». Итак, сначала преобразуйте нашу последовательность кортежей в последовательность действий записи, которую CommaJoinerя назвал, затем для этого действия добавьте, operator+чтобы соединить два действия так, как мы хотим, добавив "," между ними:

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

Беглый взгляд на Godbolt показывает, что это тоже неплохо компилируется, все звонки thunks сглаживаются.

Однако для работы с пустым кортежем потребуется вторая перегрузка.

тахсмит
источник