Как Скомпилировать Декоратор — C++, Python И Собственная Реализация. Часть 1

Данная серия статей (а их, как оказалось, две) будет посвящена возможностям создания декоратор на языке C++, особенности их работы на Python, а также рассмотрит один из вариантов реализации данного функционала на собственном компилируемом языке, используя общий подход к созданию замыканий - преобразование замыканий и модернизацию синтаксического дерева.

Вторая часть уже доступна: Здесь.



Как скомпилировать декоратор — C++, Python и собственная реализация.
</p><p>
 Часть 1

Отказ от ответственности В этой статье под декоратором подразумевается не шаблон проектирования, а декоратор в 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); }

Все работает, все нормально и вообще УРА.

На самом деле, это решение имеет несколько проблем.

Начнем по порядку:

  1. Этот код можно собрать только с версией C++14 и выше, поскольку заранее узнать тип возвращаемого значения невозможно.

    К сожалению, мне с этим жить и других вариантов я не нашла.

  2. make требует, чтобы ему была передана функция std::function, а передача функции по имени приводит к ошибкам компиляции.

    А это совсем не так удобно, как хотелось бы! Мы не можем написать такой код:

    Decorator::make([](){}); Decorator::make(myFunc); void(*ptr)(int) = myFunc; Decorator::make(ptr);

  3. Также невозможно декорировать метод класса.

Поэтому после небольшого разговора с коллегами был придуман следующий вариант для C++17 и выше:

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-функции

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.