Блог пользователя goldvitaly

Автор goldvitaly, 11 лет назад, По-русски

Всем привет! Думаю, что многие слышали, что в 2011 году вышел новый стандарт языка программирования c++. Возможности языка с новым стандартом существенно расширены, благодаря чему стало возможно делать довольно удобные вещи. Когда появляется что-то новое, то сразу хочется это попробовать на чем-нибудь полезным.

Проект testlib.hpp содержит реализацию библиотеки для разработки чекеров и генератов для олимпиадных задач. Идея разработать такую библиотеку совсем не новая, развитие множества таких библиотек происходит уже больше десяти лет. Такие библиотеки есть для огромного количества языков программирования. В России все начиналось с паскаля, потом появились библиотеки и для c++, python, java.

Зачем нужна еще одна библиотека? Затеяв разработку этой библиотеки, я преследовал множество целей.

  1. Первая цель была учебная. Эту библиотеку полностью разработали два человека: riadwaw и map. Мне кажется, что эта цель удалась. Ребятам потребовалось узнать много нового не только в языке c++ и его новом стандарте, но и в целом был получен полезный опыт общих принципов программирования.
  2. Второй целью была расширяемость. Под расширяемостью здесь я подразумеваю возможность добавлять функциональность без изменения кода библиотеки. Это принцип, который используется при создании всех общих библиотек и фраймворков.
  3. Реализация современных интерфейсов, принятых в языке 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);

Библиотеку можно расширять двумя способами

  1. добавление новой функциональности в библиотеку. Это всегда не так просто, так как библиотека довольно сложная и не многие захотят в ней разбираться. Хотя развитие этой библиотеки на мой субъективный взгляд довольно интересно и познавательно.
  2. добавление новых пользовательских классов, которые автоматические станут поддерживаться библиотекой без ее изменения. (немного об этом есть в tutorial). Этот способ крайне прост и практически не требует понимания внутреннего устройства библиотеки. Это может быть оформлено как отдельный файл, который может распространяться отдельно, либо быть включен в библиотеку. Разве не хочется в чекере писать просто inf.read<Point> и получать сразу точку на плоскости?

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

  • Проголосовать: нравится
  • +51
  • Проголосовать: не нравится

»
11 лет назад, # |
  Проголосовать: нравится -21 Проголосовать: не нравится

Назвали бы поток ответа anf по аналогии с ouf. Многие в решениях часто используют переменную ans, и иногда возникают проблемы, когда копируешь код решения в генератор или чекер. В последних контестах я уже стал писать так:

#define ans anf
#include <testlib.h>
#undef ans
  • »
    »
    11 лет назад, # ^ |
      Проголосовать: нравится +8 Проголосовать: не нравится

    Такое название уже скорее дань традиции. Эти переменные имеют совсем небольшую область видимости, то есть при желании использовать локальную переменную ans ее можно объявлять в отдельных функциях.

  • »
    »
    11 лет назад, # ^ |
      Проголосовать: нравится -8 Проголосовать: не нравится

    Если немного помедитировать, они должны называться input, user, jury

    • »
      »
      »
      11 лет назад, # ^ |
        Проголосовать: нравится +8 Проголосовать: не нравится

      Возможно. Но на такое изменение названия мы бы все равно не решились.

»
11 лет назад, # |
  Проголосовать: нравится -8 Проголосовать: не нравится

Космические корабли бороздят просторы мирового океана, а вы используете макрос для объявления функции main в чекере. После этого, если честно, сложно себя заставить что-то читать. Один вопрос: зачем?

  • »
    »
    11 лет назад, # ^ |
      Проголосовать: нравится +10 Проголосовать: не нравится

    Макросы в c++ используются крайне ограничено, но по-прежнему активно. Используются они в том числе и для разработки космических кораблей. Как и всегда, макросы используются для удобства. У нас есть и другие макросы, которые я не постесняюсь написать тут:

    WA("Incorrect answer " << a);
    OK("OK " << n << " numbers");
    
    • »
      »
      »
      11 лет назад, # ^ |
        Проголосовать: нравится +16 Проголосовать: не нравится

      Почему не

      log << "Incorrect answer " << a << wa();
      log << "OK " << n << " numbers" << ok();
      
      • »
        »
        »
        »
        11 лет назад, # ^ |
          Проголосовать: нравится +8 Проголосовать: не нравится

        Интересный вариант. Признаться он нам не пришел в голову. Я рассматривал вариант: wa << "Incorrect answer" << a, но мы не придумали как его реализовать.

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

        Я не сторонник предубеждений и табу. Полный запрет на макросы я не поддерживаю. Во всем должен быть разумный подход.

        • »
          »
          »
          »
          »
          11 лет назад, # ^ |
            Проголосовать: нравится +5 Проголосовать: не нравится

          Полный запрет на макросы я не поддерживаю хотя бы из-за случая _T().

          После некоторых раздумий, кажется правильно вот такой вариант:

          #include "testlib.hpp" //testlib может и совсем забрать себе функцию main
          
          struct Input{ //замечательный задел под валидатор!
             void read(){
             }
          };
          
          struct Answer{ //при чтении зачастую хочется ans-файл тоже валидировать
             long long magicValue;
          };
          
          template<>
          InputSource &InputSource::read<Answer>(Answer &result) //перегрузка, возможно, правильна и не в таком стиле. Пишу наугад
          {
              result.magicValue = result.read<int>();
              return *this;
          }
          
          checker_t check_it() //checker_main конечно название лучше
          {
              Answer user = ouf.read<Answer>(); //мне не очень нравится шаблонная нотация, но она лучше, чем все текущие версии. 
              Answer jury = ans.read<Answer>();
              return user.magicValue == jury.magicValue ?
                  wa(comment() << "Expected " << jury.magicValue << "but found" << user.magicValue) :
                  ok(comment() << "Answer is " << user.magicValue); //думаю, нечто подобное делается и сейчас тем самым макросом.
          }
          
          • »
            »
            »
            »
            »
            »
            11 лет назад, # ^ |
              Проголосовать: нравится +5 Проголосовать: не нравится

            я правильно понимаю, что в этом случае ouf, ans и inf -- глобальные переменные? Хорошо ли это с точки зрения космических кораблей?

            А функция main в библиотеке мне кажется не очень удачной, так как в таком случае этой библиотекой нельзя будет воспользоваться другим образом. Сейчас библиотеку можно использовать не только для написания чекеров и валидаторов, а возможно и для каких-то других целей.

            • »
              »
              »
              »
              »
              »
              »
              11 лет назад, # ^ |
              Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится
              #include "testlib_nomain.hpp" //решает проблему с main
              

              Глобальные переменные технически не должны быть проблемой в маленьких программах. Хорошо, давайте точку входа в чекер напишем так

              checker_t check_it (InfFile &inf, OufFile &ouf, AnsFile &ans)
              

              Причем все XXXFile просто наследуют одно и то же (разные во избежание ошибки в их порядке). Заодно решается проблема с именами файлов — кто как хочет, так и называет.

              На самом деле, когда я писал чекеровалидаторы, я просто копировал заготовку чекера для A+B.

              • »
                »
                »
                »
                »
                »
                »
                »
                11 лет назад, # ^ |
                  Проголосовать: нравится 0 Проголосовать: не нравится

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

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

        • »
          »
          »
          »
          »
          11 лет назад, # ^ |
            Проголосовать: нравится 0 Проголосовать: не нравится

          Можно сделать operator() у объекта wa, который будет возвращать поток, который в деструкторе проставляет соответствующий код возврата и завершает программу. Тогда использование будет выглядеть примерно так: wa() << "Incorrect answer" << a;

          • »
            »
            »
            »
            »
            »
            11 лет назад, # ^ |
              Проголосовать: нравится 0 Проголосовать: не нравится

            этот вариант мы обдумывали, но завершать работу и бросать exception в деструкторе -- плохая практика.

            Если в одном из << бросится исключение, то вызовется деструктор wa и программа будет завершена из деструктора. При этом будет весьма странное сообщение, и вообще по стандарту поведение будет неопределенным.

            • »
              »
              »
              »
              »
              »
              »
              11 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится

              Да, мне это тоже не понравилось в моей идее. По стандарту исключение из деструктора, который вызван во время обработки исключения, вызовет std::terminate. Можно пытаться в деструкторе потока проверять std::current_exception (раз С++11 всё равно требуется), но способ по кривости может соперничать с макросами.

              Тем не менее, Ваше решение с макросом имеет один минус, который можно исправить либо через variadic macros argument (который не будет работать нормально с пустым аргументом не на gcc), либо отказавшись от макросов: WA("Wrong answer " << f(x, y));

              • »
                »
                »
                »
                »
                »
                »
                »
                11 лет назад, # ^ |
                  Проголосовать: нравится 0 Проголосовать: не нравится

                а как последнее WA("Wrong answer " << f(x, y)) реализовать без макроса? Мы же именно так и сделали, но с макросом. Как сделать это без макроса я не знаю.

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  11 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

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

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  11 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  запятые в аргументах шаблонах плохи, это правда

  • »
    »
    11 лет назад, # ^ |
      Проголосовать: нравится +8 Проголосовать: не нравится

    По поводу макроса именно для функции main, то это альтернатива глобальным переменным. Можно обсудить конструктивные предложения. Как по-другому создать три потока, чтобы не было глобальных переменных? Все варианты, которые у меня получилось придумать крайне не удобны.

»
11 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Я так понимаю, что этот тестлиб будет использоваться в яндекс.контесте? Мы сейчас разрабатываем задачи для сентябрьского гран-при, но пока все на этапе идей. Если Яндекс.контестом он будет поддерживаться, то мы перейдем на него.

  • »
    »
    11 лет назад, # ^ |
      Проголосовать: нравится +8 Проголосовать: не нравится

    Чекеры и валидаторы — это программы, которые сообщают свой результат кодом возврата и иногда xml-кой. Данная библиотека полностью совместима в этом смысле с testlib.h и testlib.pas. То есть любая тестирующая система, которая работала с чекерами автоматически будет поддерживать и эту библиотеку. Поэтому использование этой библиотеки никак не связана с поддержкой тестирующей системы. Есть только ограничение на компиляторы. Так как это testlib будущего, то его можно скомпилировать пока только с помощью g++-4.7 c флагом -std=c++11