В коде решения для 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.
Ну а вот и сама шаблонная функция:
template< typename 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; for( auto & 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 копипастить пришлось бы больше, т.к. там еще и обработка ошибок бы сильно раздула бы код.
Комментариев нет:
Отправить комментарий