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

Это продолжение моего предыдущего вопроса о красивых контейнерах 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, разделители пар), что и в предыдущем вопросе!


person Kerrek SB    schedule 05.06.2011    source источник
comment
Кто-нибудь поместил здесь какой-либо код в библиотеку? Или даже .hpp-with-everything-in, который можно было взять и использовать?   -  person einpoklum    schedule 16.07.2015
comment
@einpoklum: Может быть, cxx-prettyprint? Вот для чего мне был нужен этот код.   -  person Kerrek SB    schedule 16.07.2015
comment
Отличный вопрос, и +1 за то, что я не буду обременять вас своим сломанным кодом, хотя я удивлен, что он, кажется, действительно преуспел в отражении бессмысленных орды, которые вы пробовали.   -  person Don Hatch    schedule 19.04.2020


Ответы (12)


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

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

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


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

// 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)...};
}
person Xeo    schedule 05.06.2011
comment
@Kerrek: Сейчас я тестирую и чиню себя, но на Ideone получаются странные результаты. - person Xeo; 06.06.2011
comment
Я думаю, вы также путаете потоки и строки. Вы пишете что-то похожее на std :: cout ‹< std :: cout. Другими словами, TuplePrinter не имеет operator<<. - person Kerrek SB; 06.06.2011
comment
@Kerrek: Да, я сделал довольно странные вещи ... Новая версия отредактирована и работает так, как было запрошено. :П - person Xeo; 06.06.2011
comment
Гранд, теперь это работает! Теперь я попытаюсь включить это в проект симпатичного принтера из предыдущего вопроса. Огромное спасибо! - person Kerrek SB; 06.06.2011
comment
@Kerrek: Чтобы добавить разделители из предыдущего вопроса, вам просто нужна еще одна частичная спецификация кортежей. Включил то, что сейчас. Остальное (basic_ostream и т. Д.) Должно быть легко с этого момента. - person Xeo; 06.06.2011
comment
Может быть, не так хорошо работает с пустыми кортежами, но это достаточно легко исправить с помощью другой специализации или проверки размера в операторе верхнего уровня ‹< ...? - person Nate Kohl; 06.06.2011
comment
Я не собирался иметь дело с пустыми кортежами, я достаточно счастлив, что это работает для oneples ;-) Эй, может кто-нибудь из вас, не принадлежащих к GNU, проверить, работают ли псевдонимы шаблонов в проекте симпатичного принтера? Это значительно упростит определение настраиваемых разделителей. - person Kerrek SB; 06.06.2011
comment
@Kerrek: вы также можете определить псевдонимы шаблонов в C ++ 03, как показано здесь. :) - person Xeo; 06.06.2011
comment
Ха-ха, jehova :-) Что ж, если вы посмотрите на код prettyprinter.h, мне нужен удобный способ для пользователей определить эти классы-разделители для TChar = {char, wchar_t}, поэтому я в основном хочу предоставить ярлыки sdelims и wsdelims , не делая материал более подробным. Псевдонимы шаблонов C ++ 0x звучат неплохо, но я открыт для предложений. Имейте в виду, что мой пример уже содержит демонстрацию настраиваемых разделителей, поэтому цель здесь - сделать его более элегантным. - person Kerrek SB; 06.06.2011
comment
Разве мы не можем заменить struct TuplePrinter только шаблонными функциями при использовании C ++ 0x / C ++ 11? - person Nordlöw; 24.09.2011
comment
@ Nordlöw: Нет, мы все еще не можем легко, потому что функции все еще не могут быть частично специализированы, но посмотрите мое обновление о способах обойти это, даже в C ++ 03. - person Xeo; 24.02.2012
comment
Если у вас нет вариативных шаблонов, вы можете использовать класс Tuple вместо class ... Args и std :: tuple_size ‹Tuple› :: value вместо sizeof ... (Args). - person Thomas; 20.03.2013
comment
@Thomas: Вы не можете просто использовать class Tuple для operator<< перегрузки - его бы выбрали для всех без исключения вещей. Для этого потребуется ограничение, которое как бы подразумевает необходимость в каких-то вариативных аргументах. - person Xeo; 20.03.2013
comment
Черт возьми, моя компиляция не была завершена, когда я опубликовал. Думаю, мне нужно перенести boost / tuple_io.hpp на std :: tuple. - person Thomas; 21.03.2013
comment
@Xeo: у кода есть две проблемы: а) operator<< должен возвращать std::basic_ostream<Ch,Traits>& вместо std::ostream& и б) он не работает с пустыми кортежами, т.е. std::tuple<>. - person Daniel Frey; 30.03.2013
comment
@DanielFrey: возвращаемый тип был фрагментом рефакторинга, спасибо, что это уловили. И да, пустой кортеж в настоящее время не работает, но его легко исправить. Фактически, мне, вероятно, следует переписать эту вещь, чтобы просто использовать уловку с индексами. - person Xeo; 30.03.2013
comment
@Xeo: Использовать индексы здесь будет непросто, но попробуйте. Основной проблемой будет порядок оценки. - person Daniel Frey; 30.03.2013
comment
@DanielFrey: Проблема решена, инициализация списка гарантирует порядок слева направо: swallow{(os << get<Is>(t))...};. - person Xeo; 30.03.2013
comment
@Xeo: Благодаря вашему совету использовать списки инициализаторов, чтобы гарантировать порядок выполнения, у меня есть элегантная версия с готовыми индексами. Изменить ваш ответ или написать отдельный? - person Daniel Frey; 30.03.2013
comment
@Xeo Я позаимствовал вашу ласточку для cppreference, если вы не против. - person Cubbi; 17.05.2013
comment
Некоторые придирки ... Вы не хотите определять операторы в неправильном пространстве имен, и вы не можете определить этот оператор в правильном пространстве имен (::std). Было бы лучше в качестве вызываемой функции (возможно, квалифицированной), чтобы избежать неудачного поиска оператора при поиске. - person David Rodríguez - dribeas; 02.12.2014
comment
@ DavidRodríguez-dribeas Действительно, у вас должно быть пространство имен print_pretty с шаблоном выражения pretty<T> с перегруженным <<, которое рекурсивно обрабатывает красивые итерации печати и подобные кортежи, возвращаясь к ostream << t, а затем ostream << to_string(t) с включенным Koenig std, с максимальной длиной строки и отступы, табличное отображение длинных списков и тому подобное. В итоге вы получаете библиотеку, а не ТАК-ответ. - person Yakk - Adam Nevraumont; 26.05.2015

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

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, и мы получили то, что у меня есть.

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

У меня это нормально работает на C ++ 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;
}
person Tony Olsson    schedule 04.07.2013
comment
Да, это выглядит разумно - возможно, с другой специализацией для пустого кортежа для полноты. - person Kerrek SB; 04.07.2013
comment
@KerrekSB, нет простого способа распечатать кортежи в c ++ ?, в функции python неявно возвращает кортеж, и вы можете просто распечатать их в c ++, чтобы вернуть несколько переменных из функции, которую мне нужно упаковать, используя std::make_tuple() . но во время печати в main() он выдает кучу ошибок !, Есть ли предложения по более простому способу печати кортежей? - person Anu; 15.01.2019

Я удивлен, что реализация 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)

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

person AndyG    schedule 29.06.2015

На основе кода 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)
person user5673656    schedule 26.01.2019

Используя 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();
}
person DarioP    schedule 16.10.2019

На основе примера на Язык программирования 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!")
person CW Holeman II    schedule 12.11.2015

Другой, похожий на @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 << ")";
}
person Gabriel    schedule 30.12.2014

Мне нравится ответ 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;
}
person user2445507    schedule 13.05.2020

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

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

Техника состоит в том, чтобы использовать то, что уже дает вам складка: особый случай для одного элемента. То есть, свёртка одного элемента просто расширяется до elem[0], затем 2 элемента - это elem[0] + elem[1], где + - некоторая операция. Мы хотим, чтобы один элемент записывал только этот элемент в поток, а для большего количества элементов делали то же самое, но присоединялись к каждому с дополнительной записью «,». Итак, сопоставив это с складкой С ++, мы хотим, чтобы каждый элемент был действием записи некоторого объекта в поток. Мы хотим, чтобы наша + операция заключалась в чередовании двух операций записи с записью ",". Итак, сначала преобразуйте нашу последовательность кортежей в последовательность действий записи, 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 сглаживаются.

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

person tahsmith    schedule 11.06.2020

Вот код, который я недавно сделал для печати кортежа.

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

Используя ваш примерный случай:

auto a = std::make_tuple(5, "Hello", -0.1); 
cout << a << '\n'; // (5, Hello, -0.1)
person xKaihatsu    schedule 30.11.2020

Я вижу ответы, использующие std::index_sequence с C ++ 17, однако лично я бы пошел не по этому пути. Я бы предпочел рекурсию и constexpr if:

#include <tuple>
#include <iostream>

template<std::size_t I, class... Ts>
void doPrintTuple(const std::tuple<Ts...>& tuples) {
    if constexpr (I == sizeof...(Ts)) {
        std::cout << ')';
    }
    else {
        std::cout << std::get<I>(tuples);
        if constexpr (I + 1 != sizeof...(Ts)) {
            std::cout << ", ";
        }
        doPrintTuple<I + 1>(tuples);
    }
}

template<class... Ts>
void printTuple(const std::tuple<Ts...>& tuples) {
    std::cout << '(';
    doPrintTuple<0>(tuples);
}

int main() {
    auto tup = std::make_tuple(1, "hello", 4.5);
    printTuple(tup);
}

Вывод:

(1, hello, 4.5)
person Marc Dirven    schedule 25.05.2021