Всем привет! Думаю, что многие слышали, что в 2011 году вышел новый стандарт языка программирования c++. Возможности языка с новым стандартом существенно расширены, благодаря чему стало возможно делать довольно удобные вещи. Когда появляется что-то новое, то сразу хочется это попробовать на чем-нибудь полезным.
Проект testlib.hpp содержит реализацию библиотеки для разработки чекеров и генератов для олимпиадных задач. Идея разработать такую библиотеку совсем не новая, развитие множества таких библиотек происходит уже больше десяти лет. Такие библиотеки есть для огромного количества языков программирования. В России все начиналось с паскаля, потом появились библиотеки и для c++, python, java.
Зачем нужна еще одна библиотека? Затеяв разработку этой библиотеки, я преследовал множество целей.
- Первая цель была учебная. Эту библиотеку полностью разработали два человека: riadwaw и map. Мне кажется, что эта цель удалась. Ребятам потребовалось узнать много нового не только в языке c++ и его новом стандарте, но и в целом был получен полезный опыт общих принципов программирования.
- Второй целью была расширяемость. Под расширяемостью здесь я подразумеваю возможность добавлять функциональность без изменения кода библиотеки. Это принцип, который используется при создании всех общих библиотек и фраймворков.
- Реализация современных интерфейсов, принятых в языке c++. Поддержка совместимости с stl, поддержка stream интефейсов << и >>.
Если первая цель удалась, то что же получилось со второй и третьей я расскажу описанием самой библиотеки.
Начнем с простого примера:
#include "testlib.hpp"
TESTLIB_CHECK(){
verifyEqual(ouf.read<int>(), ans.read<int>());
OK("1 number");
}
Отличия от testlib.h на этом примере не столь существенны. Однако видно, что чтение реализовано шаблонной функцией. Возможно это не привычно, но позволяет расширять библиотеку.
Посмотрим еще один пример, в котором происходит считывание вектора. Приведено лишь два варианта, существуют и другие с указанием разделителей.
#include "testlib.hpp"
#include <vector>
using namespace std;
TESTLIB_CHECK(){
//Считывает вектор из n чисел, разделенных пробелом
vector<long long> v1 = ouf.read<vector<long long>>(n);
//Считывает вектор из n чисел, с ограничениями на каждое число от 1 до 100
vector<long long> v2 = ouf.read<vector<long long>>(n,
make_default_reader<long long>(1, 100));
}
На текущий момент поддерживается чтение целых чисел, чисел с плавающей точкой, pair
, vector
, string
.
Метод read<string>
по умолчанию читает token, однако любой объект можно считать альтернативным способом. Для этого нужно первым аргументом передать Reader
.
Если нужно часто считывать какой-то объект с недефолтным reader
, то можно воспользоваться классом Alias
:
typedef Alias<string, LineReader> Line;
//теперь
string nextLine = inf.read<Line>();
//эквивалентно записи
string nextLine = inf.read<string>(LineReader());
//или
LineReader reader;
string nextLine = inf.read<string>(reader);
Библиотеку можно расширять двумя способами
- добавление новой функциональности в библиотеку. Это всегда не так просто, так как библиотека довольно сложная и не многие захотят в ней разбираться. Хотя развитие этой библиотеки на мой субъективный взгляд довольно интересно и познавательно.
- добавление новых пользовательских классов, которые автоматические станут поддерживаться библиотекой без ее изменения. (немного об этом есть в tutorial). Этот способ крайне прост и практически не требует понимания внутреннего устройства библиотеки. Это может быть оформлено как отдельный файл, который может распространяться отдельно, либо быть включен в библиотеку. Разве не хочется в чекере писать просто
inf.read<Point>
и получать сразу точку на плоскости?
Текущую версию в одном файле можно скачать отсюда. Данный файл автосгенерирован, поэтому, если вы хотите посмотреть на устройство проекта или хотите решить не поучавствовать ли в разработке, то лучше смотреть репозиторий. Там же вы найдете чуть более подробную документацию.
Назвали бы поток ответа anf по аналогии с ouf. Многие в решениях часто используют переменную ans, и иногда возникают проблемы, когда копируешь код решения в генератор или чекер. В последних контестах я уже стал писать так:
Такое название уже скорее дань традиции. Эти переменные имеют совсем небольшую область видимости, то есть при желании использовать локальную переменную ans ее можно объявлять в отдельных функциях.
Если немного помедитировать, они должны называться
input
,user
,jury
Возможно. Но на такое изменение названия мы бы все равно не решились.
Космические корабли бороздят просторы мирового океана, а вы используете макрос для объявления функции main в чекере. После этого, если честно, сложно себя заставить что-то читать. Один вопрос: зачем?
Макросы в c++ используются крайне ограничено, но по-прежнему активно. Используются они в том числе и для разработки космических кораблей. Как и всегда, макросы используются для удобства. У нас есть и другие макросы, которые я не постесняюсь написать тут:
Почему не
Интересный вариант. Признаться он нам не пришел в голову. Я рассматривал вариант:
wa << "Incorrect answer" << a
, но мы не придумали как его реализовать.Мне сейчас трудно определиться какой интерфейс мне нравится больше. В этом варианте запись все-таки происходит задом наперед, что немного неестественно. Мы можем попробовать это обсудить с аудиторией тут и с другими разработчиками.
Я не сторонник предубеждений и табу. Полный запрет на макросы я не поддерживаю. Во всем должен быть разумный подход.
Полный запрет на макросы я не поддерживаю хотя бы из-за случая _T().
После некоторых раздумий, кажется правильно вот такой вариант:
я правильно понимаю, что в этом случае ouf, ans и inf -- глобальные переменные? Хорошо ли это с точки зрения космических кораблей?
А функция main в библиотеке мне кажется не очень удачной, так как в таком случае этой библиотекой нельзя будет воспользоваться другим образом. Сейчас библиотеку можно использовать не только для написания чекеров и валидаторов, а возможно и для каких-то других целей.
Глобальные переменные технически не должны быть проблемой в маленьких программах. Хорошо, давайте точку входа в чекер напишем так
Причем все XXXFile просто наследуют одно и то же (разные во избежание ошибки в их порядке). Заодно решается проблема с именами файлов — кто как хочет, так и называет.
На самом деле, когда я писал чекеровалидаторы, я просто копировал заготовку чекера для A+B.
мы себе искуственно ввели ограничение, что testlib распространяется одним файлом. другой вариант по нашим прогнозам будет очень сложно распространять, поэтому testlib_nomain нам не подойдет.
Этот вариант с функцией мы рассматривали, но все же нам показалось это не удобным. Уж очень много символов нужно написать. Нам нужно, чтобы написание чекера не усложнилось, а даже наоборот упростилось.
Можно сделать operator() у объекта wa, который будет возвращать поток, который в деструкторе проставляет соответствующий код возврата и завершает программу. Тогда использование будет выглядеть примерно так: wa() << "Incorrect answer" << a;
этот вариант мы обдумывали, но завершать работу и бросать exception в деструкторе -- плохая практика.
Если в одном из << бросится исключение, то вызовется деструктор wa и программа будет завершена из деструктора. При этом будет весьма странное сообщение, и вообще по стандарту поведение будет неопределенным.
Да, мне это тоже не понравилось в моей идее. По стандарту исключение из деструктора, который вызван во время обработки исключения, вызовет std::terminate. Можно пытаться в деструкторе потока проверять std::current_exception (раз С++11 всё равно требуется), но способ по кривости может соперничать с макросами.
Тем не менее, Ваше решение с макросом имеет один минус, который можно исправить либо через variadic macros argument (который не будет работать нормально с пустым аргументом не на gcc), либо отказавшись от макросов: WA("Wrong answer " << f(x, y));
а как последнее
WA("Wrong answer " << f(x, y))
реализовать без макроса? Мы же именно так и сделали, но с макросом. Как сделать это без макроса я не знаю.Я имел в виду, что код, где в параметре макроса есть запятые, может скомпилироваться неправильно или с ошибкой там, где запятая синтаксически допустима как оператор. Но с данным примером я наврал -- он работает верно.
запятые в аргументах шаблонах плохи, это правда
По поводу макроса именно для функции main, то это альтернатива глобальным переменным. Можно обсудить конструктивные предложения. Как по-другому создать три потока, чтобы не было глобальных переменных? Все варианты, которые у меня получилось придумать крайне не удобны.
Я так понимаю, что этот тестлиб будет использоваться в яндекс.контесте? Мы сейчас разрабатываем задачи для сентябрьского гран-при, но пока все на этапе идей. Если Яндекс.контестом он будет поддерживаться, то мы перейдем на него.
Чекеры и валидаторы — это программы, которые сообщают свой результат кодом возврата и иногда xml-кой. Данная библиотека полностью совместима в этом смысле с testlib.h и testlib.pas. То есть любая тестирующая система, которая работала с чекерами автоматически будет поддерживать и эту библиотеку. Поэтому использование этой библиотеки никак не связана с поддержкой тестирующей системы. Есть только ограничение на компиляторы. Так как это testlib будущего, то его можно скомпилировать пока только с помощью g++-4.7 c флагом -std=c++11