Недавно на глаза попалось два случая очень удачного, на мой взгляд, использования макросов языка C++. Полагаю, что гуру C++ это все знают и умеют, но т.к. сам я никогда гуру не был, да еще и многое забыл, то лучше зафиксирую в склерозник в виде этого блог-поста.
Первый фокус подсмотрел в библиотеке Catch. Эта header-only библиотека предлагает еще один подход к реализации unit-тестирования в C++. Оформленный с помощью Catch код теста очень сильно напоминает то, что в свое время я делал на Ruby с Ruby-новыми блоками кода. Вот пример из туториала Catch-а:
TEST_CASE( "vectors can be sized and resized", "[vector]" ) { std::vector<int> v( 5 ); REQUIRE( v.size() == 5 ); REQUIRE( v.capacity() >= 5 ); SECTION( "resizing bigger changes size and capacity" ) { v.resize( 10 ); REQUIRE( v.size() == 10 ); REQUIRE( v.capacity() >= 10 ); } SECTION( "resizing smaller changes size but not capacity" ) { v.resize( 0 ); REQUIRE( v.size() == 0 ); REQUIRE( v.capacity() >= 5 ); } |
Как раз когда я увидел Catch, мне нужно было выкинуть из своего проекта ACE-овские макросы для логирования. В качестве замены захотелось иметь возможность писать так, как в Catch. Пришлось разбираться. Оказалось, что никакой особой магии там нет. За макросами вроде SECTION скрывается if() внутри которого объявляется переменная какого-то внутреннего Catch-вского класса. Что-то вроде:
#define INTERNAL_CATCH_SECTION( name, desc ) \ if( Catch::Section const& INTERNAL_CATCH_UNIQUE_NAME( catch_internal_Section ) \ = Catch::SectionInfo( CATCH_INTERNAL_LINEINFO, name, desc ) ) |
Т.е. внутри SECTION прячется if(), а то, что идет после SECTION внутри фигурных скобок, оказывается просто частью кода if-а. Просто, красиво, эффективно.
У меня, правда, с одним if-ом не прокатило, т.к. мне нужно было объявлять две переменные двух разных типов (одна содержит всю информацию о месте, из которого осуществляется логирование, вторая является экземпляром std::ostringstream для формирования текста сообщения). Поэтому мне пришлось прятать под макросом два for():
#define SO_5_LOG_ERROR_IMPL(logger, file, line, var_name) \ for( so_5::log_msg_details::conductor_t conductor__( logger, file, line ); \ !conductor__.completed(); ) \ for( std::ostringstream & var_name = conductor__.stream(); \ !conductor__.completed(); conductor__.log_message() ) #define SO_5_LOG_ERROR(logger, var_name) \ SO_5_LOG_ERROR_IMPL(logger, __FILE__, __LINE__, var_name ) |
Во внешнем for-е объявляется переменная типа conductor_t, в которой хранится все: и ostringstream-объект, и имя файла, и номер строки. Во внутреннем for-е объявляется ссылка на ostringstream из conductor-а. Именно с этой переменной имеет дело программист. Когда внутренний for завершает первую итерацию, conductor выполняет логирование и выставляет признак того, что логирование выполнено. Этот признак не дает внутреннему циклу уйти на вторую итерацию. А затем точно так же прерывает и внешний цикл.
Использование SO_4_LOG_ERROR в коде выглядит вот так:
SO_5_LOG_ERROR( a_exception_producer.so_environment(), log_stream ) { log_stream << "An exception '" << x.what() << "' during processing unhandled exception '" << ex_to_log.what() << "' from cooperation '" << a_exception_producer.so_coop_name() << "'. Application will be aborted."; } |
Второй фокус с макросом и активным использованием фич С++11 внутри него я подсмотрел буквально на днях в презентации "С++11 in the Wild" с CppCon2014. Если кто-то ее еще не смотрел, то очень рекомендую. Имхо, из просмотренных мной одна из самых полезных и интересных.
Так вот, авторы этой презентации представили макрос Auto, который похож на ON_EXIT в различных ипостасях, но, в отличии от остальных плюсовых вариантов, реально удобен в использовании. Позволяет писать простом и понятном стиле:
FILE * f = fopen( file_name, "r" ); if( f ) { Auto( fclose(f) ); ... } |
Но что больше всего меня поражает в реализации макроса Auto, так это компактность, простота и, при этом всем, наличие хитростей, незаметных на первый взгляд, но делающих этом макрос очень эффективным. Как, например, отказ от использования std::function. Собственно, вот весь код этого макроса.
#pragma once template <class Lambda> class AtScopeExit { Lambda& m_lambda; public: AtScopeExit(Lambda& action) : m_lambda(action) {} ~AtScopeExit() { m_lambda(); } }; #define TOKEN_PASTEx(x, y) x ## y #define TOKEN_PASTE(x, y) TOKEN_PASTEx(x, y) #define Auto_INTERNAL1(lname, aname, ...) \ auto lname = [&]() { __VA_ARGS__; }; \ AtScopeExit<decltype(lname)> aname(lname); #define Auto_INTERNAL2(ctr, ...) \ Auto_INTERNAL1(TOKEN_PASTE(Auto_func_, ctr), \ TOKEN_PASTE(Auto_instance_, ctr), __VA_ARGS__) #define Auto(...) Auto_INTERNAL2(__COUNTER__, __VA_ARGS__) |
За пояснением деталей рекомендую обратиться к уже указанной презентации -- там все подробно и по делу, без воды и заумностей.
Ну вот как-то так. Можно и на C++ вполне себе нормально программировать, не выстраивая трехэтажных шаблонных конструкций и не отстреливая ноги. И это пока еще C++11 не до всех дошел еще. А уже не за горами и C++14 с еще большими вкусностями :)
Комментариев нет:
Отправить комментарий