пятница, 27 мая 2016 г.

[prog.c++] Тяпничное по поводу современного C++

Последние пару месяцев плотно работаю с C++14 (на том уровне, который поддерживается в GCC-5.2 и 5.3). Конечно же C++14 -- это небольшое улучшение над C++11, но в общем и целом, если сравнивать с C++03, современный C++ является совершенно другим языком. Уж не знаю, как C++14 ощущается в задачах, где нужно выжимать такты из битов и наоборот. Но вот у меня сейчас C++ что-то вроде клеевого языка, основная задача которого экономить ресурсы, коих на целевых машинах не то, чтобы уж много. И ощущается C++14 в таком контексте практически так же, как какой-нибудь Ruby или Python. С приблизительно такими отличиями:

  • для C++ не так уж много хороших и живых библиотек, а те, что есть, нужно подключать в свой проект сильно по разному (впрочем, здесь MxxRu::externals ну очень сильно упрощает жизнь);
  • проходит гораздо больше времени между внесениями изменений в код и первым запуском измененного кода. В Ruby, например, не нужно ждать, пока проект с туевой хучей шаблонов из header-only библиотек скомпилируется и слинкуется;
  • но уж если C++ный код скомпилировался, то не приходится ожидать вылета ошибки из-за несовпадения типов переменных или еще какой-нибудь мелочевки, которая в языках со статической компиляцией вылавливается на стадии компиляции, а в динамических языка -- в run-time;
  • если уж твой C++ный код тормозит, то только из-за тебя :)

В общем, если затащить в свой проект качественные библиотеки и есть возможность пользоваться нормальным компилятором, то жить с C++11/14 легко и весело. Особенно если увлечься шаблонами и auto, то C++ный код по читабельности для меня оказывается ну очень похожим на Ruby-новый код: точно так же сначала все просто и очевидно, а через пару недель чешешь голову стараясь понять, какой же тип у этого выражения ;) В качестве демонстрации покажу ниже небольшой фрагментик (что называется, "из недавнего").

Но к чему я это все? А к тому, что оглядываясь по сторона возникает некоторое недоумение. Иногда задумываешься: куда же катится этот мир? Взять, например, русскоязычные профильные форумы. Мало того, что активность вокруг C++ там не такая уж и активная. Так еще и вопросы такие всплывают, что невольно задаешься вопросом: а вменяемые C++ники вообще остались? Глядя на вот такое (или такое, или такое) напрашивается вывод о том, что нас практически-то и не осталось. Если же задуматься еще и о том, что за такие художества кому-то еще и деньги платят, то как-то совсем грустно...

А еще давеча в почтовом ящике письмо из Boost-announce нашел, с темой "Rebooting Boost: Call for Lightning Talks @ CppCon". Вот, процитирую оттуда:

Recent and recurrent boost-dev discussions have raised the lack of cmake based tooling; lack of ABI management and the consequent ODR violation when mixing Boost versions in the same process; the anti-social behaviour of Boost towards library end users, new ideas, new blood and the wider C++ community; and the chronic lack of maintenance of up to half the Boost libraries.

One question which needs to be answered is whether a clean reboot of Boost from the first principles of proving high quality C++ libraries well suited for use with the latest C++ standard is viable. This new collection of C++ libraries would be started completely from scratch (even if some existing libraries were ported into the new organisation), so ANYTHING is possible.

Т.е. у Boost-а куча проблем, поэтому давайте-ка подумаем, как начать все заново.

Стало еще грустнее. На мой взгляд, Boost-оводы сделали много хорошего, но за это придется платить слишком большую цену. Подход, при котором из Boost-а пытаются сделать централизованную коллекцию высококлассных библиотек, был спорным с самого начала. Но раньше он хотя бы был оправдан тем, C++ развивался крайне медленно и в Boost-е обкатывались многие базовые вещи, которые следовало бы иметь в языке гораздо раньше.

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

Уже неоднократно говорил о том, что C++у нужно брать на вооружение идеи из других языков. RubyGems из Ruby или Cargo и Rust-а. Вот что нужно C++ сейчас, а не монстроузный Boost. Но, к сожалению, только у Boost-а достаточный авторитет, чтобы сделать какую-то систему управления зависимостями для C++ де-факто стандартом.

В общем, несколько досадно, что в кои-то веки C++ стал действительно хорошим и удобным языком. А вот инфраструктура вокруг него как была болотом, так и осталась.

Ну а теперь обещанные слайды :)

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

Интересно, как этот код воспринимается не C++никами. Для меня, например, он мало отличается от того, чтобы я написал для такой же задачи на Ruby (разве что в Ruby синтаксис был бы другой).

templatetypename EXIT_WAITER >
void
try_generate_new_exception(
   // Функтор, который будет ждать завершения работы дочернего процесса.
   EXIT_WAITER exit_waiter,
   // Исключение, которое выскочило при работе процесса.
   const std::exception & x,
   // Сам процесс.
   procxx::process & tool )
{
   // Попытаемся вычитать поток ошибок от внешнего инструмента.
   auto stderr_content = load_tool_stderr( tool );

   // Нужно дождаться завершения процесса, чтобы у нас была возможность
   // залогировать его код возврата.
   auto exit_code = exit_waiter();

   // Если процесс завершился с ненулевым кодом возврата, то нужно
   // сгенерировать новое исключение, в котором этот факт будет отражен.
   if( exit_code )
   {
      auto exception_message = stderr_content.empty() ?
            fmt::format( "non-zero exit code: {}, no stderr, "
                  "associated exception: [{}]", exit_code, x.what() ) :
            fmt::format( "non-zero exit code: {}, stderr: [{}]",
                  exit_code, stderr_content );
      throw std::runtime_error( std::move(exception_message) ); 
   }
}

templatetypename DEADLINE_MAKER >
auto
launch_tool_and_parse_output(
   DEADLINE_MAKER deadline_maker,
   procxx::process & tool,
   tool_output_parser_t & parser,
   spdlog::logger & logger )
{
   auto wait_and_trace_exit_code = [&] {
      tool.wait();
      logger.trace( "tool finished [exit_code={}]", tool.code() );
      return tool.code();
   };

   tool.exec();
   // Сразу после запуска начинаем контролировать время жизни
   // запущенного процесса.
   auto tool_deadline = deadline_maker( tool.id() );

   try
   {
      parser.parse( tool.output(), [&logger]( const auto & line ) {
         logger.trace( "tool_stdout: {}", line );
      } );
   }
   catchconst std::exception & x )
   {
      // Если процесс был завершен принудительно, то дальнейшая обработка
      // не нужна.
      if( deadline_status_t::process_finished_itself != tool_deadline.status() )
      {
         wait_and_trace_exit_code();
         throw std::runtime_error( "tool killed because of deadline" );
      }

      // Ну а раз процесс завершился самостоятельно, то имеет смысл
      // прочитать его поток ошибок и разобраться с кодами возврата.
      try_generate_new_exception( wait_and_trace_exit_code, x, tool );

      // У tool нулевой код возврата и мы не знаем, что делать.
      // Пусть с исключением разбираются наверху.
      throw;
   }
   catch( ... )
   {
      // Неожиданно... Но нужно дождаться завершение процесса и залогировать
      // его код возврата.
      wait_and_trace_exit_code();
      // После чего с исключением пусть разбираются выше.
      throw;
   }

   // Если оказались здесь без ошибок, значит процесс отработал нормально.
   // Но дожаться его завершения все равно нужно.
   return wait_and_trace_exit_code();
}

void
a_action_performer_t::run_child_process_and_collect_results(
   const msg_ns::new_action & action,
   msg_ns::action_result & result )
{
   m_logger.trace( "initiating action, [action_id={}][max_total_time={}s]",
         result.m_action_id, action.m_max_total_time );

   // Подготавливаем процесс к запуску...
   auto tool = prepare_tool_process( action, result.m_action_id );

   // ...собираем и парсим выхлоп...
   tool_output_parser_t parser( result.m_action_id );
   auto exit_code = launch_tool_and_parse_output(
         [&]( pid_t pid ) {
            const auto deadline = calc_tool_deadline( action );
            return process_deadline_t(
                  so_environment(), m_child_deadliner, pid, deadline,
                  fmt::format( "[action_id={}][deadline={}s]",
                        result.m_action_id, deadline.count() ) );
         },
         *tool, parser, m_logger );

   // Переносим в результат те значения, которые удалось разобрать.
   fill_result( exit_code, parser.result(), result );
}

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