Chain of Responsibility на C++ variadic templates

habr.png

Речь пойдёт о таком простом, но часто используемом паттерне как chain of responsibility (цепочка ответственности). Суть паттерна в том, что для обработки какого либо события мы используем несколько обработчиков, каждый из которых принимает решение о том, что и когда передавать следующему. В сети есть масса примеров реализации на C++, но я хочу показать реализацию только на лямдба-выражениях. В этой реализации можно будет посмотреть немного уличной template-magic.
Итак, допустим у нас есть объект:

class Elephant
{
public:
   std::string touch_leg()
   {
      return "it's like a pillar";
   }
   std::string touch_trunk()
   {
      return "it's like a snake";
   }
   std::string touch_tail()
   {
      return "it's like a rope";
   }
   void run_away()
   {
      m_is_gone = true;
      std::cout << "*** Sound of running out elephant ***\n";
   }
   bool is_elephant_here()
   {
      return !m_is_gone;
   }
private:
   bool m_is_gone = false;
};


И у нас будут 3 обработчика, каждый из которых будет передавать объект далее:

// Создаём цепочку ответственности и добавляем первый обработчик
auto blind_sage3 = ChainOfRepsonsibility::start_new([](Elephant& e) {
  std::cout << "Third blind sage: " << e.touch_tail() << "\n";
});
// "Сверху" предыдущего устанавливаем новый обработчик, который всегда передаёт дальше
auto blind_sage2 = blind_sage3.attach([](Elephant& e, auto& next) {
  std::cout << "Second blind sage: " << e.touch_trunk() << "\n";
  next(e);
});
// Устанавливаем ещё один обработчик, который может либо передать дальше, либо остановить обработку
auto blind_sage1 = blind_sage2.attach([](Elephant& e, auto& next) {
  if (!e.is_elephant_here())
  {
         std::cout << "First blind sage: So empty... so true\n";
  }
  else
  {
         std::cout << "First blind sage: " << e.touch_leg() << "\n";
         next(e);
  }
});
// Создаём объект и начинаем обрабатывать
Elephant e;
blind_sage1(e);


В этом примере есть 3 обработчика, каждый из которых лямбда-выражение, которые объединены в цепочку ответственности с помощью класса ChainOfRepsonsibility.

Вот сама реализация класса:

#include 

struct ChainOfRepsonsibility
{
   template
   struct Chain
   {
      template
      Chain(const Callee c, const Next& n)
      {
         m_impl = c;
         m_next = n;
      }
      template
      decltype(auto) attach(Callee c)
      {
         return Chain(c, *this);
      }
      void operator()(Args... e)
      {
         m_impl(e..., m_next);
      }
      std::function)> m_impl;
      std::function m_next;
   };

   template
   struct ChainTail
   {
      template
      ChainTail(Callee c)
      {
         m_impl = c;
      }
      template
      decltype(auto) attach(Callee c)
      {
         return Chain(c, m_impl);
      }
      void operator()(Args... e)
      {
         m_impl(e...);
      }
      std::function m_impl;
   };

   template
   struct StartChain;

   template
   struct StartChain
   {
      using Type = ChainTail;
   };

   template
   static decltype(auto) start_new(Callee c)
   {
      return StartChain::Type(c);
   }
};


Работает оно так:

  • Вначале мы создаём цепочку ответственности с помощью функции start_new. Основная проблема на этом этапе — это добыть из переданной лямбды список аргументов и создать на их основе «прототип» обработчика.
  • Для доставания аргументов лямбды используется класс StartChain и хитрый прём с специализацией шаблонов. Вначале мы декларируем общий вариант класса, а потом специализацию struct StartChain. Эта конструкция позволяем нам получить доступ к списку аргументов по переданному члену класса.
  • Имея список аргументов мы уже можем создавать классы ChainTail и Chain, которые будут обёрткой обработчиков. Их задача сохранить в std: function лямбду и вызвать её в нужный момент, а также реализовать метод attach (установку обработчика сверху).


Код проверен на visual studio 2015, для gcc возможно надо будет поправить специализацию StartChain убрав оттуда const, если кроме лямбд хочеться передавать просто функции, то можно добавить ещё одну специализацию StartChain. Можно ещё пробовать избавиться копирования в attach, делать move, но чтобы не усложнять пример, я оставил только самый простой случай.

© Habrahabr.ru