Как скомпилировать декоратор — C++, Python и собственная реализация. Часть 1
- вторник, 2 июня 2020 г. в 00:33:57
Данная серия статей будет посвящена возможности создания декоратора в языке С++, особенностям их работы в Python, а также будет рассмотрен один из вариантов реализации данного функционала в собственном компилируемом языке, посредством применения общего подхода для создания замыканий — closure conversion и модернизации синтаксического дерева.
Все началось с того, что мой товарищ VoidDruid решил в качестве диплома написать небольшой компилятор, ключевой фичей которого являются декораторы. Еще во время предзащиты, когда он расписывал все преимущества его подхода, заключавшегося в изменении AST, мне стало интересно: а неужели в великом и могучем С++ невозможно реализовать эти самые декораторы и обойтись без всяких сложных терминов и подходов? Прогуглив эту тему, я не нашел никаких простых и общих подходов к решению данной проблемы (к слову сказать, попадались лишь только статьи про реализацию паттерна проектирования) и тогда засел за написание своего собственного декоратора.
Однако перед тем, как перейти к непосредственному описанию моей реализации, я бы хотел рассказать немного про то, как устроены лямбды и замыкания в С++ и какая между ними разница. Сразу оговорюсь, что если нет никаких упоминаний конкретного стандарта, то по умолчанию имеется в виду С++20. Если говорить коротко, то лямбды – это анонимные функции, а замыкания, функции, которые используют объекты из своего окружения. Так например, начиная с С++11, лямбду можно объявить и вызвать так:
int main()
{
[] (int a)
{
std::cout << a << std::endl;
}(10);
}
int main()
{
auto lambda = [] (int a)
{
std::cout << a << std::endl;
};
lambda(10);
}
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);
int main()
{
int a = 10;
auto closure = [&a] () { a += 1; };
closure();
}
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<void()> function = closure;
function();
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)...);
};
}
};
void myFunc(int a)
{
std::cout << "here" << std::endl;
}
int main()
{
std::function<void(int)> f = myFunc;
auto decorated = Decorator::make(f);
decorated(10);
}
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)...
);
};
}
};
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));
}
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)...
);
};
}
};