Композиционные декораторы функций C++
В Python есть очень полезная функция декораторов функций, которая, кроме того, позволяет создавать композиции. Например, если написать функцию foo
тогда вы можете заявить, что хотели бы foo
быть запомненным, но также повторяться более одного раза в случае отсутствия кэша, в котором foo
также выдвигает исключение:
@lru_cache
@retry
def foo(...):
Компоновка декоратора позволяет разрабатывать такие функции, как foo
и индивидуальные декораторы функций независимо, а затем смешивая их по мере необходимости. Было бы хорошо, если бы мы могли сделать это и в C++ (насколько это возможно).
Хотя в Stackru есть несколько вопросов, касающихся декораторов функций, все они, похоже, генерируют несоставимые из-за жестких предположений о сигнатуре декорированной функции. Например, рассмотрим отличный ответ на этот вопрос. Украшение имеет форму
template <typename R, typename... Args>
std::function<R (Args...)> memo(R (*fn)(Args...)) {
Следовательно, он не может быть применен к самому результату (по общему признанию, не большая проблема для конкретного декоратора, использующего мемоизацию).
Как тогда мы можем написать декораторы составных функций?
2 ответа
Другой способ создания составных декораторов функций - использование набора классов смешивания.
Следует минимальный рабочий пример:
#include<iostream>
#include<functional>
#include<utility>
#include<type_traits>
template<class T>
struct LoggerDecoratorA: public T {
template<class U>
LoggerDecoratorA(const U &u): T{u} { }
template<typename... Args>
auto operator()(Args&&... args) const ->
typename std::enable_if<
not std::is_same<
typename std::result_of<T(Args...)>::type,
void
>::value,
typename std::result_of<T(Args...)>::type>::type
{
using namespace std;
cout << "> logger A" << endl;
auto ret = T::operator()(std::forward<Args>(args)...);
cout << "< logger A" << endl;
return ret;
}
template<typename... Args>
auto operator()(Args&&... args) const ->
typename std::enable_if<
std::is_same<
typename std::result_of<T(Args...)>::type,
void
>::value,
typename std::result_of<T(Args...)>::type>::type
{
using namespace std;
cout << "> logger A" << endl;
T::operator()(std::forward<Args>(args)...);
cout << "< logger A" << endl;
}
};
template<class T>
struct LoggerDecoratorB: public T {
template<class U>
LoggerDecoratorB(const U &u): T{u} { }
template<typename... Args>
auto operator()(Args&&... args) const ->
typename std::enable_if<
not std::is_same<
typename std::result_of<T(Args...)>::type,
void
>::value,
typename std::result_of<T(Args...)>::type>::type
{
using namespace std;
cout << "> logger B" << endl;
auto ret = T::operator()(std::forward<Args>(args)...);
cout << "< logger B" << endl;
return ret;
}
template<typename... Args>
auto operator()(Args&&... args) const ->
typename std::enable_if<
std::is_same<
typename std::result_of<T(Args...)>::type,
void
>::value,
typename std::result_of<T(Args...)>::type>::type
{
using namespace std;
cout << "> logger B" << endl;
T::operator()(std::forward<Args>(args)...);
cout << "< logger B" << endl;
}
};
int main() {
std::function<int()> fn = [](){
using namespace std;
cout << 42 << endl;
return 42;
};
std::function<void()> vFn = [](){
using namespace std;
cout << "void" << endl;
};
using namespace std;
decltype(fn) aFn =
LoggerDecoratorA<decltype(fn)>(fn);
aFn();
cout << "---" << endl;
decltype(vFn) bVFn =
LoggerDecoratorB<decltype(vFn)>(vFn);
bVFn();
cout << "---" << endl;
decltype(fn) abFn =
LoggerDecoratorA<LoggerDecoratorB<decltype(fn)>>(fn);
abFn();
cout << "---" << endl;
decltype(fn) baFn =
LoggerDecoratorB<LoggerDecoratorA<decltype(fn)>>(fn);
baFn();
}
Я не уверен, что из упомянутых вами проблем действительно решается, но не стесняйтесь просить об изменениях, и я постараюсь обновить его, если это возможно.
Одним из способов создания составных функций декораторов является ослабление предположения о сигнатуре, принятой декоратором. Скажем у нас
template<class Fn>
struct foo_decorator
{
template<typename ...Args>
auto operator()(Args &&...args) const ->
typename std::result_of<Fn(Args...)>::type;
};
template<class Fn>
foo_decorator<Fn> make_foo(const Fn &fn) {return foo_decorator<Fn>();}
Как видно, оба make_foo
а также foo_decorator
параметризованы class Fn
который может быть практически любым в этой точке. Следовательно, они могут взять и лямбда-функцию или функтор, например. Взятые аргументы (и возвращаемый тип) (время компиляции) откладываются до вызова, где выведенные параметры шаблона при вызове функции C++ будут заполнять остальные детали по мере необходимости.
Используя это, вот простой декоратор регистрации:
#include <type_traits>
#include <cmath>
#include <iostream>
template<class Fn>
struct logger
{
logger(const Fn &fn, const std::string &name) : m_fn(fn), m_name{name}{}
template<typename ...Args>
auto operator()(Args &&...args) const ->
typename std::result_of<Fn(Args...)>::type
{
std::cout << "entering " << m_name << std::endl;
const auto ret = m_fn(std::forward<Args>(args)...);
std::cout << "leaving " << m_name << std::endl;
return ret;
}
private:
Fn m_fn;
std::string m_name;
};
template<class Fn>
logger<Fn> make_log(const Fn &fn, const std::string &name)
{
return logger<Fn>(fn, name);
}
int main()
{
auto fn = make_log([](double x){return std::sin(x);}, "sin");
std::cout << fn(4.0) << std::endl;
}
Вот сборка этого декоратора, вот сборка повторного декоратора, а вот сборка их композиции.
Одним из недостатков этого подхода является случай, когда декоратор имеет состояние, основанное на сигнатуре функции, например, оригинальный случай запоминания. С этим можно справиться, используя стирание типа (см. Сборку здесь), но у этого есть ряд недостатков, один из которых заключается в том, что ошибки, которые концептуально могли быть обнаружены во время компиляции, теперь обнаруживаются во время выполнения (когда стирание типа обнаруживает незаконное использование).