Как-то на алгосах мне стало лень читать входные данные…
Как в плюсах можно читать входные данные, если не совсем руками?
iostream
cstdio
- Известным в узких кругах
optimization.h
cstdio
и iostream
обладают для меня крайней неприятным стилем - сначала определять переменную, а потом читать в неё. В cstdio
надо ещё и продублировать информацию в формат:
int a, b;
"%d %d", &a, &b);
scanf(
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<int>());
f(read()); // g(read<long>()); g(read());
Тоесть, мы хотим перегуржать функцию по её возвращаемому значению. Если попытаться что-то нибудь такое погуглить, все будут говорить что так нельзя, но это неправда.
На то как именно это можно сделать, я наткнулся абсолютно случайно читая про 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
).
Код целиком (вместе с аналогичной штукой для вывода), можно посмотреть тут