Во многих гуидах написано, что лямбда выражения в с++ это всего лишь синтаксический сахар для анонимных функторов. Но при обработке, скажем, такого:
auto lam = [](int x)->int {return x;}
или, даже, такого:
function<int(int)> lam = [](int x)->int {return x;}
выражения, происходит ли замена лямбда выражения на этот самый функтор, сгенерированный на лету при начальном этапе компиляции?
Т.е генерирует ли компилятор непосредственный класс функтора с определенным оператором вызова (), подобно тому, как она конкретезирует шаблоны? Или же он не геренерирует такого класса вовсе, а обработка и превращение лямбд во что-то подобное функторам происходит где-то там внизу, на этапе составления AST деревьев и всего этого CFG SSA и прочего, в чем я не разбираюсь от слова совсем?
И дополнительно, можно ли увидеть простую реализацию шаблонного класса function, который как-то попределяет оператор =, так чтобы можно было его использовать для приравнивания лямбда-выражению? примерно такое:
MyFunctionImpl<int(int)> my_func = [](int x)->int {return x;}
Только отвечайте, пожалуйста, если точно знаете, а не предполагаете.
Компилятор генерирует класс с operator()
. Это записано в стандарте.
Но впоследствии, во многих сценариях использования функтора, будет выполнена inline-подстановка в точке вызова этого оператора. С практической точки зрения, неважно как компилятор генерирует код, важно что в точке использования будет сразу использовано значение x из аргумента, буз вызова функции.
Подстановка тела функции и сквозная оптимизация (через границу вызова) наверняка будет выполнена, если в точке вызова известен конечный класс переменной lam
и доступно определение вызываемой функции (operaotr()
). Т.е.
auto lam = [](int x)->int {return x;}
y=lam(42) ; // здесь подстановка будет выполнена
template<typename T>
int foo(T lam) { return lam(42); }
...
y=foo( [](int x)->int {return x;} ); // здесь подстановка будет выполнена
Реализация std::function определяется конкретным автором реализации stl, но по сути, он предназначен для стирания информации о типе, поэтому при вызове функтора через него подстановка наверняка не произойдет.
std::function<int(int)> lam = [](int x)->int {return x;}
y=lam(42) ; // здесь подстановка НЕ будет выполнена
int foo(std::function<int(int)> lam) { return lam(42); }
...
y=foo( [](int x)->int {return x;} ); // здесь подстановка НЕ будет выполнена
Происходит ли замена лямбда выражения на этот самый функтор, сгенерированный на лету при начальном этапе компиляции?
Да, происходит. Это вы можете увидеть это по сгенерированному листингу. Вызов структуры F
и лямбды одинаков.
Т.е генерирует ли компилятор непосредственный класс функтора с определенным оператором вызова ()
, подобно тому, как она конкретезирует шаблоны?
Дописав еще одну, идентичную лямбду, видно, что компилятор заманглил имена по разному, что свидетельствует о генерации двух разных функций, у разных анонимных типов
P.S Я пользовался сайтом https://godbolt.org , он очень удобен для демонстрации и проверки выхлопа различных компиляторов.
Ответ: да, генерация такого класса с определяемым реализацией именем и публичным методом operator ()
действительно происходит. Однако это не более чем деталь реализации. Именно в разделе о лямбда-выражениях стандарт языка открытым текстом говорит, что реализации могут реализовывать лямбды и по-другому, пока наблюдаемое поведение совпадает с требуемым.
Но даже в тривиальном варианте реализация лямбд тоже может содержать какое-то количество "компиляторной магии". Например, объект замыкания без захвата является приводимым к обыкновенному указателю на функцию
void (*p)() = []{};
но в то же время, как мы знаем, метод operator ()
в С++ не может являться статическим. То есть выполнение вышеприведенного преобразования является чем-то большим, чем просто "синтаксическим сахаром". Но для компилятора ничего сложного здесь нет.
Что касается реализации классов MyFunctionImpl<int(int)>
... они основаны на применении техники "type erasure" и минимальный пример такого все-таки будет довольно громоздок.
Некая наивная/элементаная реализация может выглядеть так
template <typename> class MyFunctionImpl;
template <typename R, typename ...ARGS>
class MyFunctionImpl<R(ARGS ...)>
{
public:
template <typename F> MyFunctionImpl(F &&f) :
impl(new SpecificImpl<F>(std::forward<F>(f)))
{}
R operator ()(ARGS ...args)
{ return (*impl)(std::forward<ARGS>(args)...); }
private:
struct Impl
{
virtual R operator ()(ARGS ...args) = 0;
};
template <typename F> struct SpecificImpl : Impl
{
F f;
template <typename U> SpecificImpl(U &&f) : f(std::forward<U>(f))
{}
R operator ()(ARGS ...args) override
{ return f(std::forward<ARGS>(args)...); }
};
std::unique_ptr<Impl> impl;
};
Пример использования
int foo(int i)
{
return i * 2;
}
int main()
{
MyFunctionImpl<int(int)> mf1 = foo;
int i = mf1(10);
std::cout << i << std::endl;
MyFunctionImpl<int(int)> mf2 = [](int i) { return i * 3; };
i = mf2(10);
std::cout << i << std::endl;
}
http://coliru.stacked-crooked.com/a/60e9a654788e2378
Айфон мало держит заряд, разбираемся с проблемой вместе с AppLab
у меня есть несколько элементов, события на которые нужно обрабатывать похожим образом, как поставить единый обработчик на их общего предкаТ