Chain of Responsibility на C++ variadic templates

в 5:03, , рубрики: c++, chain of responsibility, patterns, variadic templates, Программирование

Речь пойдёт о таком простом, но часто используемом паттерне как 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 truen";
  }
  else
  {
	 std::cout << "First blind sage: " << e.touch_leg() << "n";
	 next(e);
  }
});
// Создаём объект и начинаем обрабатывать
Elephant e;
blind_sage1(e);

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

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

#include <functional>

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

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

   template<typename>
   struct StartChain;

   template<typename C, typename... Args>
   struct StartChain<void (C::*)(Args...) const>
   {
      using Type = ChainTail<Args...>;
   };

   template<typename Callee>
   static decltype(auto) start_new(Callee c)
   {
      return StartChain<decltype(&Callee::operator())>::Type(c);
   }
};

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

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

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

Автор: taxx

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js