Template IO

Posted on 2021-07-09

Как-то на алгосах мне стало лень читать входные данные…

Как в плюсах можно читать входные данные, если не совсем руками?

  • iostream
  • cstdio
  • Известным в узких кругах optimization.h

cstdio и iostream обладают для меня крайней неприятным стилем - сначала определять переменную, а потом читать в неё. В cstdio надо ещё и продублировать информацию в формат:

int a, b;
scanf("%d %d", &a, &b);

int a, b;
std::cin >> a >> b;

optimization.h работает в несколько раз быстрее других (что с учётом таймлимитов Копелиовича критично в ~половине задач), и обладает более приятным стилем:

int a = readInt(), b = readInt();

Из минусов, из-за аггресивного кэширования, почему-то не на всех компах (или, скорее, не на всех терминалах) optimization.h понимает EOF, в результате использующая его программа может просто зависнуть на чтении, что мешает тестировать локально.

Второй минус optimization.h - необохдимость явно указывать тип при четнии, что в принципе не так сложно, и можно избегать дублирования используя auto, но неприятно:

int a;
long b;
double c;
char d;

cin >> a >> b >> c >> d;

int a = readInt();
long b = readInt<long>();
double c = readDouble();
char d = readChar();

В какой-то момент я на это посмотрел, и понял на - C++ можно выразить гораздо более… страшный извращённый бесполезный универсальный стиль применения. И это превратилась в “Сколько разных видов поведения я смогу запихать в одну функцию?”. Представляю templateio.h:

auto a = read<int>();
auto [b1, b2] = read<long long, char>();
auto [c1, c2, c3] = read<3, int>();
int d = read();
long e = read();
double f = read();

Во всех этих выражениях читается точный тип (тоесть, например e читатется как long, а не как int).

Как добиться такого извращения? Пойдём по порядку:

Обычный read<T>()

Самый базовый способ - мы точно знаем тип, надо просто его прочитать.

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

namespace template_io {
    namespace input {
        template<typename T, typename = void>
        struct reader {};

        template<typename T>
        struct reader<T, std::enable_if_t<std::is_integral_v<T>>> {
            static auto func() {
                return readInt<T>();
            }
        };

        template<>
        struct reader<double> {
            static auto func() {
                return readDouble();
            }
        };

        template<typename T>
        auto read() {
            return reader<T>::func();
        }
    }
}

Теперь если хочется читать новый тип (например, vector<T>), то надо просто в нужном нэймспйэсе написать специализацию. А если специализации нет, всё падает на этапе компиляции.

read<T1, ..., TN>() и read<N, T>():

Часто хочется за одно действие считать несколько элементов ввода. Для этого надо как-то пробросить произвольное число типов, развренуть это в вызовы read<T>() и собрать всё это в tuple:

К счастью, variadic templates были сделаны именно для такого:

namespace template_io {
    namespace input {
        template<typename ...Ts, typename = std::enable_if_t<sizeof...(Ts) >= 2>>
        auto read() {
            return std::tuple{read<Ts>()...};
        }
    }
}

enable_if вешаем чтобы точно не было конфликтов с read<T>(). Конструктор через фигурные скобки гарантирует порядок вычисления аргументов, а RVO гарантирует что никаких лишних копирований нет.

Как-же теперь сделать read<T, N>()? Сходу на вариадиках такого не напишешь придётся ловить ту ускользающую грань безумия через которую прокидываются аргументы

namespace template_io {
    namespace meta {
        template<typename ...Ts>
        struct type_list {};

        template<typename T, size_t N>
        class make_n_list {
            template<typename = std::make_index_sequence<N>>
            struct impl;

            template<size_t... Is>
            struct impl<std::index_sequence<Is...>> {
                template<size_t>
                using wrap = T;

                using value = type_list<wrap<Is>...>;
            };

        public:
            using value = typename impl<>::value;
        };
        
        template<typename T, size_t N>
        using make_n_list_v = typename make_n_list<T, N>::value;
    }
}

type_list<T1, ... TN> - пустая структура которая хранит всю информацию в своём типе. make_n_list<T, N> позволяет её конструировать:

Сначала число N превращаем в std::index_sequence<1, ..., N> и используем его как дефолтный параметр шаблона для impl. impl принимает его, и вытаскивает ...Is = 1, ..., N. wrap - обёртка, чтобы была “зависимость” от индекса, и можно было применить .... wrap<1> = wrap<k> = T, значит wrap<Is>... = wrap<1>, ..., wrap<N> = T, ..., T. Это и кладём в type_list. Остаток кода просто сахар.

Теперь, начинаем прокидывать:

namespace template_io {
    namespace input {
        template<typename ...Ts, typename = std::enable_if_t<sizeof...(Ts) >= 1>>
        auto read(meta::type_list<Ts...>) {
            return read<Ts...>();
        }
    }
}

Эта функция предполагается внутренней, хотя по желанию можно вызывать руками. Отдельная красота в том, то если Ts состоит из одного типа, то вызовется read<T>() (который сразу читает), а если больше - read<T1, ..., TN>().

И, наконец, необходимая нам функция:

namespace template_io {
    namespace input {
        template<size_t N, typename T = int>
        auto read() {
            return read(meta::make_n_list_v<T, N>());
        }
    }
}

“Перегрузка” по возвращаемому значению

Остался последний шаг. Он проще, но извращёнее.

Мы хотим получить такую ситуацию:

int a = read(); // read<int>();
long b = read(); // read<long>();
double c = read(); // read<double>();

И даже такую:

void f(int a) {};
void g(long b) {};

f(read()); // f(read<int>());
g(read()); // g(read<long>());

Тоесть, мы хотим перегуржать функцию по её возвращаемому значению. Если попытаться что-то нибудь такое погуглить, все будут говорить что так нельзя, но это неправда.

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

То что нам надо - шаблонный оператор приведения типов. Конкретно, наш read() будет возвращать тип, который можно скастовать к абсолютно любому типу для которого определён read<T>():

namespace template_io {
    namespace input {
        struct read_s {
            template<typename T>
            operator T() {
                return read<T>();
            }
        };
        
        auto read() {
            return read_s();
        }
    }
}

Тогда, когда мы пытаемся сделать int a = read()

  • read() создаёт read_s()
  • Предположим что происходит вызов operator= для int (для примитивов это не так, но поведение похоже)
  • operator= хочет вторым аргументов тоже int
  • Происходит попытка скастовать read_s неявно к int
  • У read_s есть оператор неявно преобразования
  • Подставляется T=int, и происходит вызов read<int>()

Такая магия. Из известных проблем:

Если кто-то уже определил специализацию reader для типа, преопределить её нельзя.

read() читает тогда и только тогда, когда структура к чему-то кастуется. Если не уследить, можно получить что-нибудь такое:

auto a = read(); // auto = read_s
return a + a; // Хотели сложить с самим собой, но получилось read() + read()

Особенно такое веселье может произойти если передать в какую-нибудь шаблонную функцию.

Не бенчмаркал вообще. Но вроде не должно быть значительно дольше чем бэкэнд (специализации reader).

Код целиком (вместе с аналогичной штукой для вывода), можно посмотреть тут