вторник, 8 октября 2013 г.

[prog.c++11] Пример преобразования кода с использованием lambda-функций

Расскажу о том, как сегодня переделывал функцию и, в итоге, задействовал ставшие доступными в C++11 лямбда-функции для борьбы с дублированием кода.

Внутри SObjectizer-5 был метод следующего вида:

void
so_environment_impl_t::run(
   so_environment_t & env,
   throwing_strategy_t throwing_strategy )
{
   m_layer_core.start();

   m_disp_core.start();

   m_timer_thread->start();

   m_agent_core.start();

   bool init_threw = false;
   std::string init_exception_reason;
   try
   {
      env.init();
   }
   catchconst std::exception & ex )
   {
      init_threw = true;
      init_exception_reason = ex.what();
      env.stop();
   }

   m_agent_core.wait_for_start_deregistration();

   m_agent_core.finish();

   m_timer_thread->finish();

   m_disp_core.finish();

   m_layer_core.finish();

   if( init_threw )
   {
      SO_5_THROW_EXCEPTION(
         rc_environment_error,
         "init() failed: " + init_exception_reason );
   }
}

Этот метод не обеспечивал безопасности по исключениям. Т.е. если, например, m_agent_core.start() бросал исключение, то не выполнялись откаты выполненных до этого действий (не вызывались методы finish() у m_timer_thread, m_disp_core и m_layer_core). Для исправления этой ситуации все последовательно идущие действия были преобразованы в последовательность мелких методов, каждый из которых вызывает всего один метод start(), передает управление следующему методу, а после возврата оттуда вызывает метод finish(). Получилось что-то вроде:

void
so_environment_impl_t::run(
   so_environment_t & env )
{
   try
   {
      run_layers_and_go_further( env );
   }
   catchconst so_5::exception_t & )
   {
      // Rethrow our exception because it already has all information.
      throw;
   }
   catchconst std::exception & x )
   {
      SO_5_THROW_EXCEPTION(
            rc_environment_error,
            std::string( "some unexpected error during "
                  "environment launching: " ) + x.what() );
   }
}

void
so_environment_impl_t::run_layers_and_go_further(
   so_environment_t & env )
{
   m_layer_core.start();

   try
   {
      run_dispatcher_and_go_further( env );
   }
   catchconst std::exception & x )
   {
      m_layer_core.finish();
      throw;
   }

   m_layer_core.finish();
}

void
so_environment_impl_t::run_dispatcher_and_go_further(
   so_environment_t & env )
{
   m_disp_core.start();

   try
   {
      run_timer_and_go_further( env );
   }
   catchconst std::exception & x )
   {
      m_disp_core.finish();
      throw;
   }

   m_disp_core.finish();

}
...

Таких мелких методов run_something_and_go_further набралось четыре штуки. И все одни были практически близнецами братьями, отличаясь только тремя строчками внутри. Понятно дело, что это уже плохой знак. Но еще хуже то, что приведенная выше первая реализация была не очень хорошей.

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

Поэтому я пошел по пути написания одного вспомогательного метода, который реализует один общий алгоритм, а различающиеся детали получает в качестве параметров. В результате функции run_something_and_go_further получились вот такими:

void
so_environment_impl_t::run_layers_and_go_further(
   so_environment_t & env )
{
   do_run_stage(
         "run_layers",
         [this] { m_layer_core.start(); },
         [this] { m_layer_core.finish(); },
         [this, &env] { run_dispatcher_and_go_further( env ); } );
}

void
so_environment_impl_t::run_dispatcher_and_go_further(
   so_environment_t & env )
{
   do_run_stage(
         "run_dispatcher",
         [this] { m_disp_core.start(); },
         [this] { m_disp_core.finish(); },
         [this, &env] { run_timer_and_go_further( env ); } );
}

А сам метод do_run_stage после нескольких итераций приобрел вид более сложный, чем я первоначально предполагал:

void
so_environment_impl_t::do_run_stage(
   const std::string & stage_name,
   std::function< void() > init_fn,
   std::function< void() > deinit_fn,
   std::function< void() > next_stage )
{
   try
   {
      init_fn();
   }
   catchconst std::exception & x )
   {
      SO_5_THROW_EXCEPTION(
            rc_unexpected_error,
            stage_name + ": initialization failed, exception is: '" +
            x.what() + "'" );
   }

   try
   {
      next_stage();
   }
   catchconst std::exception & x )
   {
      try
      {
         deinit_fn();
      }
      catchconst std::exception & nested )
      {
         SO_5_THROW_EXCEPTION(
               rc_unexpected_error,
               stage_name + ": deinitialization failed during "
               "exception handling. Original exception is: '" + x.what() +
               "', deinitialization exception is: '" + nested.what() + "'" );
      }

      throw;
   }

   try
   {
      deinit_fn();
   }
   catchconst std::exception & x )
   {
      SO_5_THROW_EXCEPTION(
            rc_unexpected_error,
            stage_name + ": deinitialization failed, exception is: '" +
            x.what() + "'" );
   }
}

Применение лямбда-функций позволило мне модифицировать всего лишь один метод do_run_stage, а не вносить однотипные изменения в четыре отдельных метода run_something_and_go_further.

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

Можно было бы задействовать старые добрые С-шные макросы. Возможно, они бы дали самую высокую скорость исполнения. А код бы выглядел как-то так:

#define DO_RUN_STAGE(stage_name, init_fn, deinit_fn, next_stage) ...

void
so_environment_impl_t::run_layers_and_go_further(
   so_environment_t & env )
{
   DO_RUN_STAGE(
         "run_layers",
         m_layer_core.start(),
         m_layer_core.finish(),
         run_dispatcher_and_go_further( env ) );
}

void
so_environment_impl_t::run_dispatcher_and_go_further(
   so_environment_t & env )
{
   DO_RUN_STAGE(
         "run_dispatcher",
         m_disp_core.start(),
         m_disp_core.finish(),
         run_timer_and_go_further( env ) );
}

Но вот отлавливать ошибки внутри макросов и сгенерированного из макросов кода мне бы не хотелось. Поэтому я бы предпочел написать шаблонную функцию, используя тот факт, что есть объекты, у которых нужно вызывать методы start и finish одинакового формата. Нет, эти объекты не принадлежат общей иерархии классов. Просто так получилось, что у них есть эти методы :) С помощью шаблонной функции могло бы получиться что-то вроде:

templateclass T, class E >
void
run_stage(
   const std::string & stage_name,
   T & stage_object,
   E * env_impl,
   void (E::*next_stage)( so_environment_t & ),
   so_environment_t & env )
{ ... }

void
so_environment_impl_t::run_layers_and_go_further(
   so_environment_t & env )
{
   run_stage(
         "run_layers",
         m_layer_core,
         this,
         &so_environment_impl_t::run_dispatcher_and_go_further,
         env );
}

void
so_environment_impl_t::run_dispatcher_and_go_further(
   so_environment_t & env )
{
   run_stage(
         "run_dispatcher",
         m_disp_core,
         this,
         &so_environment_impl_t::run_timer_and_go_further,
         env );
}

Если же есть неприятие шаблонов, то можно было бы обойтись чистым ООП (как во времена C++ начала 90-х годов прошлого века, ну или же во все еще современных Java 6 и Java 7). Для этого нужно было бы все-таки ввести некий базовый класс (интерфейс в теминологии Java) для классов, которым принадлежат объекты m_layer_core/m_disp_core/... В этом интерфейсе должны были бы быть чистые виртуальные методы start() и finish(). Тогда вспомогательный метод run_stage был бы нешаблонным, а мой код мог бы выглядеть как-то так:

// От этого класса должны быть унаследованы классы для объектов
// m_layer_core/m_disp_core/...
class service_manipulator_t
{
public :
   virtual void start() = 0;
   virtual void finish() = 0;
};


// Сделан членом класса so_environment_impl_t чтобы не нужно было
// передавать this параметром.
void
so_environment_impl_t::run_stage(
   const std::string & stage_name,
   service_manipulator_t & stage_object,
   void (so_environment_impl_t::*next_stage)( so_environment_t & ),
   so_environment_t & env )
{ ... }

void
so_environment_impl_t::run_layers_and_go_further(
   so_environment_t & env )
{
   run_stage(
         "run_layers",
         m_layer_core,
         &so_environment_impl_t::run_dispatcher_and_go_further,
         env );
}

void
so_environment_impl_t::run_dispatcher_and_go_further(
   so_environment_t & env )
{
   run_stage(
         "run_dispatcher",
         m_disp_core,
         &so_environment_impl_t::run_timer_and_go_further,
         env );
}

Кстати говоря, вот чем мне нравится C++ по сравнению с Java, так это наличием указателей на методы. При показанном выше подходе каждая стадия инициализации (т.е. run_something_and_go_further) может быть обычным методом класса. А вот в Java мне бы пришлось выдумывать что-то другое (по крайней мере до выхода Java 8 с поддержкой лямбда-функций). Возможно, что-то вроде анонимных классов-адаптеров, если я правильно помню название этого приема.

Но всю эту историю можно развернуть и в другом направлении. В моей текущей реализации для перехода к следующей стадии и возврата назад используется стек. Можно обойтись и без стека. Циклом.

Допустим, мы не против использовать лямбды в C++ (а так же имеем возможность делать это и нас не ограничивают старым C++ным компилятором для какой-то экзотической платформы). Стадии инициализации можно было бы описать структурой вида:

struct stage_info_t
{
   const char * const m_stage_name;
   std::function< void() > m_init_fn;
   std::function< void() > m_deinit_fn;
};

А обработка стадий инициализации могла бы выглядеть как-то так:

stage_into_t stages[] = {
   { "run_layers",
      [this] { m_layer_core.start(); },
      [this] { m_layer_core.finish(); } },
   { "run_dispatcher",
      [this] { m_disp_core.start(); },
      [this] { m_disp_core.finish(); } },
   ...
};

auto completed_stages = std::begin(stages);

// Выполнение инициализации для каждой стадии.
for( ; completed_stages != std::end(stages); ++completed_stages )
{
   try {
      stages[completed_stages].m_init_fn();
   }
   catchconst exception & x )
   { ... /* Какое-то сохранение информации об исключении */ }
}
if( std::end(stages) == completed_stages )
   // Все стадии пройдены успешно. Можно делать что-то
   // специфическое для этого случая.
   ...
// Выполнение деинициализации для каждой успешно инициализированной
// стадии. Деинициализация выполняется в обратном порядке.
if( std::begin(stages) != completed_stages )
{
   do
   {
      --completed_stages;
      try {
         stages[completed_stages].m_deinit_fn();
      }
      catchconst exception & x )
      { ... /* Какое-то сохранение информации об исключении */ }
   }
   while( std::begin(stages) != completed_stages );
}

// Здесь осталось проверить, были ли какие-то исключения в процессе
// инициализации или деинициализации и, если были, как-то об этом
// сообщить.

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

Так же замечу, что вариант с циклом может быть написан и на "голом ООП" без лямбда функций. Для этого нужно чтобы все объекты с методами start/finish были наследниками какого-то общего базового типа. Тогда внутрь структуры stage_info_t можно было бы помещать указатели на объекты этого типа. Но само решение с циклом выглядело бы практически точь-в-точь, как и с лямбда-функциями.

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

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