пятница, 19 сентября 2014 г.

[prog.c++] Макросы зло, но иногда...

Недавно на глаза попалось два случая очень удачного, на мой взгляд, использования макросов языка 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 с еще большими вкусностями :)

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