среда, 22 ноября 2017 г.

[prog.c++] Шаблоны против копипасты 7: вывод нужных типов из указателей на методы

В коде решения для Highload Cup-а Коли Гродзицкого обнаружился интересный способ использования C++ных шаблонов для того, чтобы избавится от копипасты. Насколько я понимаю, суть проблемы была в следующем:

Есть класс database_t, который нужно наполнить экземплярами структур user_t, location_t и visit_t. Структуры же эти вычитываются из JSON-файлов. Т.е. есть файлик вроде users_1.json, из которого нужно прочитать все JSON-объекты, преобразовать их в экземпляры user_t и запихнуть все эти экземпляры в database_t. Тоже самое нужно сделать с visits_1.json (структуры visit_t) и locations_1.json (структуры location_t).

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

void
load_users_from_file(
   database_t & db,
   const std::string & file_name )
{
   load_from_file_and_add_to_db3(
         db, file_name,
         &hlcup_data::json_tools::all_users_t::m_users,
         &database_t::add_user );
}

void
load_locations_from_file(
   database_t & db,
   const std::string & file_name )
{
   load_from_file_and_add_to_db3(
         db, file_name,
         &hlcup_data::json_tools::all_locations_t::m_locations,
         &database_t::add_location );
}

void
load_visits_from_file(
   database_t & db,
   const std::string & file_name )
{
   load_from_file_and_add_to_db3(
         db, file_name,
         &hlcup_data::json_tools::all_visits_t::m_visits,
         &database_t::add_visit );
}

Т.е. здесь все операции над файлом и его содержимым делегируются шаблонной функции load_from_file_and_add_to_db3. При этом уникальными параметрами при вызове данной функции оказываются третий и четвертый параметры. Третий параметр является указателем на член класса, который является контейнером объектов после их десериализации из JSON-файла. А четвертый параметр -- это указатель на метод типа database_t, который должен использоваться для наполнения объекта database_t соответствующими экземплярами. Можно увидеть, что четвертым параметром передаются методы add_user, add_location и add_visit.

Ну а вот и сама шаблонная функция:

templatetypename T, typename V, typename D >
void
load_from_file_and_add_to_db3(
   database_t & db,
   const std::string & file_name,
   V (T::*pointer_to_vector),
   void (database_t::*add_method)(D &&) )
{
   std::ifstream file;
   file.exceptions( std::ifstream::badbit );
   file.open( file_name );
   cpp_util_3::ensure< error_t >( file.is_open(),
         [&]{ return fmt::format( "unable to open file: {}", file_name ); } );

   T content;
   json_dto::from_stream( file, content );
   file.close();

   V & vec = content.*pointer_to_vector;
   forauto & item : vec )
      (db.*add_method)( std::move(item) );
}

И здесь происходит интересная магия. Третьим параметром является указатель на член некой структуры T. Реальный тип этого указателя не суть важен, как собственно, не суть важен и тип контейнера, на который указывает этот указатель. Но важно, что это за структура T. Этот тип T выводится автоматически из типа указателя на член T. Что дает возможность создать экземпляр T внутри load_from_file_and_add_to_db3.

Т.е. если передается указатель на hlcup_data::json_tools::all_visits_t::m_visits, то load_from_file_and_add_to_db3 получает возможность создать внутри себя экземпляр all_visits_t. Десериализовать его значение из json-файла, после чего пройтись по контейнеру all_visits_t::m_visits и переместить все содержимое контейнера в объект database_t.

Честно говоря, по первости, когда видишь этот код, возникает ощущение некого шаманства. Отрадно, что в C++ есть и шаблоны, и указатели на члены/методы классов. В языках без таких вещей (например, в Go), пришлось бы изобретать какие-то обходные пути. Ну или тупо копипастить. Причем в Go копипастить пришлось бы больше, т.к. там еще и обработка ошибок бы сильно раздула бы код.

Комментариев нет: