c++ lambda expression пр компиляции

106
14 июня 2021, 01:20

Во многих гуидах написано, что лямбда выражения в с++ это всего лишь синтаксический сахар для анонимных функторов. Но при обработке, скажем, такого:

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;}

Только отвечайте, пожалуйста, если точно знаете, а не предполагаете.

Answer 1

Компилятор генерирует класс с 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;} ); // здесь подстановка НЕ будет выполнена   
Answer 2

Происходит ли замена лямбда выражения на этот самый функтор, сгенерированный на лету при начальном этапе компиляции?

Да, происходит. Это вы можете увидеть это по сгенерированному листингу. Вызов структуры F и лямбды одинаков.

Т.е генерирует ли компилятор непосредственный класс функтора с определенным оператором вызова (), подобно тому, как она конкретезирует шаблоны?

Дописав еще одну, идентичную лямбду, видно, что компилятор заманглил имена по разному, что свидетельствует о генерации двух разных функций, у разных анонимных типов

P.S Я пользовался сайтом https://godbolt.org , он очень удобен для демонстрации и проверки выхлопа различных компиляторов.

Answer 3

Ответ: да, генерация такого класса с определяемым реализацией именем и публичным методом 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

READ ALSO
jquery keypress не видит 1 символ

jquery keypress не видит 1 символ

Подскажите, почему выводит -1 символ?

76
Делегирование событий

Делегирование событий

у меня есть несколько элементов, события на которые нужно обрабатывать похожим образом, как поставить единый обработчик на их общего предкаТ

85