Данная серия статей (а их, как оказалось, две) будет посвящена возможностям создания декоратор на языке C++, особенности их работы на Python, а также рассмотрит один из вариантов реализации данного функционала на собственном компилируемом языке, используя общий подход к созданию замыканий - преобразование замыканий и модернизацию синтаксического дерева.
Вторая часть уже доступна: Здесь.
Отказ от ответственности
В этой статье под декоратором подразумевается не шаблон проектирования, а декоратор в Python — способ изменения поведения функции.
Декоратор в Python — это функция, которая применяется к другой функции (декорируемой).
Функция-декоратор создает замыкание (новую функцию), которое вызывает декорированную функцию внутри себя и делает все, что нужно программисту (регистрацию вызовов, захват ресурсов и т. д.), а интерпретатор Python затем «привязывает» полученное замыкание к имени.
целевой функции.
Декоратор на C++
Все началось, когда мой друг VoidДруид В качестве дипломной работы я решил написать небольшой компилятор, ключевой особенностью которого являются декораторы.Еще во время предзащиты, когда он описал все преимущества своего подхода, заключавшегося в изменении AST, я начал задаваться вопросом: неужели невозможно в великом и могучем C++ реализовать эти самые декораторы и обойтись без всяких сложных терминов? и подходы? Погуглив эту тему, я не нашел простых и общих подходов к решению этой проблемы (кстати, мне попадались только статьи о реализации паттерна проектирования) и тогда я начал писать свой декоратор.
Однако, прежде чем перейти к непосредственному описанию моей реализации, хотелось бы немного рассказать о том, как структурированы лямбды и замыкания в C++ и в чем между ними разница.
Сразу оговорюсь: если не упоминается какой-то конкретный стандарт, то по умолчанию имеется в виду C++20. Короче говоря, лямбды — это анонимные функции, а замыкания — это функции, которые используют объекты из своей среды.
Например, начиная с C++11, лямбда-выражение может быть объявлено и вызвано следующим образом:
Или присвойте его значение переменной и вызовите ее позже.int main() { [] (int a) { std::cout << a << std::endl; }(10); }
int main()
{
auto lambda = [] (int a)
{
std::cout << a << std::endl;
};
lambda(10);
}
Но что происходит во время компиляции и что такое лямбда? Для того, чтобы погрузиться во внутреннее устройство лямбды, достаточно зайти на сайт cppinsights.io и запустите наш первый пример.
Далее я прикрепил возможный результат: class __lambda_60_19
{
public:
inline void operator()(int a) const
{
std::cout.operator<<(a).
operator<<(std::endl); } using retType_60_19 = void (*)(int); inline operator retType_60_19 () const noexcept { return __invoke; }; private: static inline void __invoke(int a) { std::cout.operator<<(a).
operator<<(std::endl);
}
};
Итак, при компиляции лямбда превращается в класс, а точнее в функтор (объект, для которого она определена оператор() ) с автоматически сгенерированным уникальным именем, имеющим оператор() , который принимает параметры, которые мы передали в нашу лямбду, а его тело содержит код, который наша лямбда должна выполнить.
С этим вроде бы все понятно, а как насчет двух других способов, зачем они? Первый — это оператор приведения к указателю на функцию, прототип которого совпадает с нашей лямбдой, а второй — это код, который должен выполняться при вызове нашей лямбды после предварительного присвоения ее указателю, например: void (*p_lambda) (int) = lambda;
p_lambda(10);
Ну, одной загадкой меньше, а как насчет замыканий? Давайте напишем простой пример замыкания, которое захватывает переменную «a» по ссылке и увеличивает ее на единицу.
int main()
{
int a = 10;
auto closure = [&a] () { a += 1; };
closure();
}
Как вы могли заметить, механизм создания замыканий и лямбд в C++ практически одинаков, поэтому эти понятия часто путают друг с другом и лямбды и замыкания называют просто лямбдами.
Но вернемся к внутреннему представлению замыкания в C++.
class __lambda_61_20
{
public:
inline void operator()()
{
a += 1;
}
private:
int & a;
public:
__lambda_61_20(int & _a)
: a{_a}
{}
};
Как видите, мы добавили новый конструктор, отличный от стандартного, который принимает наш параметр по ссылке и сохраняет его как член класса.
Собственно, именно поэтому вам нужно быть предельно осторожным при установке [&] или [=], потому что замыкание будет хранить весь контекст внутри себя, а это может быть весьма неоптимально для памяти.
Кроме того, мы потеряли оператор приведения к указателю на функцию, потому что теперь для ее нормального вызова нужен контекст. И теперь приведенный выше код не будет компилироваться: int main()
{
int a = 10;
auto closure = [&a] () { a += 1; };
closure();
void (*ptr)(int) = closure;
}
Однако если вам все же нужно где-то передать замыкание, то использование std::function никто не отменял.
std::function<void()> function = closure;
function();
Теперь, когда мы примерно разобрались, что такое лямбды и замыкания в C++, перейдем собственно к написанию декоратора.
Но сначала нам нужно определиться с нашими требованиями.
Итак, декоратор должен взять на вход нашу функцию или метод, добавить к нему нужный нам функционал (для примера это будет опущено) и вернуть новую функцию, при вызове которой выполняется наш код и код функции/метода.
В этот момент любой уважающий себя питонист скажет: «Ну конечно! Декоратор должен заменить исходный объект и любой вызов его по имени должен вызывать новую функцию! Это как раз основное ограничение C++, мы не можем никак запретить пользователю вызывать старую функцию.
Конечно, есть вариант получить его адрес в памяти и стереть (в этом случае обращение к нему приведет к аварийному завершению работы программы) или заменить его тело консольным предупреждением о том, что его использовать не следует, но это чревато серьезными последствиями.
Если первый вариант вообще кажется достаточно жёстким, то второй при использовании различных оптимизаций компилятора тоже может привести к сбою, и поэтому мы не будем их использовать.
Также я считаю ненужным использование здесь какой-либо макромагии.
Итак, перейдем к написанию нашего декоратора.
Первый вариант, который мне пришел в голову, был следующий: namespace Decorator
{
template<typename R, typename .
Args> static auto make(const std::function<R(Args.)>& f) { std::cout << "Do something" << std::endl; return [=](Args. args) { return f(std::forward<Args>(args).
);
};
}
};
Пусть это будет структура со статическим методом, который принимает std::function и возвращает замыкание, которое будет принимать те же параметры, что и наша функция, и при вызове просто вызовет нашу функцию и вернет ее результат.
Давайте создадим простую функцию, которую мы хотим украсить.
void myFunc(int a)
{
std::cout << "here" << std::endl;
}
И наш главный будет выглядеть так: int main()
{
std::function<void(int)> f = myFunc;
auto decorated = Decorator::make(f);
decorated(10);
}
Все работает, все нормально и вообще УРА.
На самом деле, это решение имеет несколько проблем.
Начнем по порядку:
- Этот код можно собрать только с версией C++14 и выше, поскольку заранее узнать тип возвращаемого значения невозможно.
К сожалению, мне с этим жить и других вариантов я не нашла.
- make требует, чтобы ему была передана функция std::function, а передача функции по имени приводит к ошибкам компиляции.
А это совсем не так удобно, как хотелось бы! Мы не можем написать такой код:
Decorator::make([](){}); Decorator::make(myFunc); void(*ptr)(int) = myFunc; Decorator::make(ptr);
- Также невозможно декорировать метод класса.
namespace Decorator
{
template<typename Function>
static auto make(Function&& func)
{
return [func = std::forward<Function>(func)] (auto && .
args)
{
std::cout << "Do something" << std::endl;
return std::invoke(
func,
std::forward<decltype(args)>(args).
);
};
}
};
Плюсы именно этого варианта в том, что теперь мы можем украсить абсолютно любой предмет, имеющий оператор() .
Например, мы можем передать имя свободной функции, указатель, лямбду, любой функтор, std::function и, конечно же, метод класса.
В случае последнего ему также необходимо будет передавать контекст при вызове декорируемой функции.
Варианты применения
int main()
{
auto decorated_1 = Decorator::make(myFunc);
decorated_1(1,2);
auto my_lambda = [] (int a, int b)
{
std::cout << a << " " << b <<std::endl;
};
auto decorated_2 = Decorator::make(my_lambda);
decorated_2(3,4);
int (*ptr)(int, int) = myFunc;
auto decorated_3 = Decorator::make(ptr);
decorated_3(5,6);
std::function<void(int, int)> fun = myFunc;
auto decorated_4 = Decorator::make(fun);
decorated_4(7,8);
auto decorated_5 = Decorator::make(decorated_4);
decorated_5(9, 10);
auto decorated_6 = Decorator::make(&MyClass::func);
decorated_6(MyClass(10));
}
Кроме того, этот код можно скомпилировать с помощью C++14, если существует расширение для использования std::invoke, которое необходимо заменить на std::__invoke. Если расширения нет, то от возможности декорирования методов класса придется отказаться, и этот функционал станет недоступен.
Чтобы не писать громоздкую команду «std::forward (args).
», вы можете использовать функциональные возможности, доступные в C++20, и создать наш лямбда-шаблон! namespace Decorator
{
template<typename Function>
static auto make(Function&& func)
{
return [func = std::forward<Function>(func)]
<typename .
Args> (Args && .
args) { return std::invoke( func, std::forward<Args>(args).
);
};
}
};
Все в порядке — безопасно и даже работает так, как мы хотим (или, по крайней мере, делаем вид).
Этот код скомпилирован как для версий gcc, так и для clang 10-x, и вы можете найти его здесь.
Здесь .
Также будут реализованы различные стандарты.
В следующей статье мы перейдем к канонической реализации декораторов на примере Python и их внутренней структуре.
Теги: #python #c++ #C++ #Функциональное программирование #компиляторы #замыкания #c++20 #шаблоны c++ #c++17 #c++14 #c++11 #lambda-функции
-
Как Смотреть Hulu И Netflix За Пределами Сша
19 Oct, 24 -
Герике, Отто
19 Oct, 24 -
Колония. Глава 2: Ответ На Сигнал Бедствия
19 Oct, 24 -
Один На Один: Задаем Правильные Вопросы
19 Oct, 24 -
Простой Способ Изменить Приложение Android
19 Oct, 24