Лямбда делить на скорость что это
Перейти к содержимому

Лямбда делить на скорость что это

  • автор:

Примеры лямбда-выражений

В данной статье приводится описание методов использования лямбда-выражений в программах. Обзор лямбда-выражений см . в лямбда-выражениях. Дополнительные сведения о структуре лямбда-выражения см . в лямбда-синтаксисе выражений.

Объявление лямбда-выражений

Пример 1

Так как лямбда-выражение вводится, его можно назначить переменной auto или function объекту, как показано ниже:

// declaring_lambda_expressions1.cpp // compile with: /EHsc /W4 #include #include int main() < using namespace std; // Assign the lambda expression that adds two numbers to an auto variable. auto f1 = [](int x, int y) < return x + y; >; cout f2 = [](int x, int y) < return x + y; >; cout

В примере получается следующий результат.

Замечания

Дополнительные сведения см. в разделе auto , function Класс и вызов функции.

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

Пример 2

Компилятор Microsoft C++ привязывает лямбда-выражение к захваченным переменным, когда выражение объявляется вместо вызова выражения. В следующем примере содержится лямбда-выражение, которое фиксирует локальную переменную i по значению, а локальную переменную j — по ссылке. Поскольку лямбда-выражение захватывает i по значению, переопределение i далее в программе не влияет на результат выражения. Однако, поскольку лямбда-выражение захватывает j по ссылке, переопределение j влияет на результат выражения.

// declaring_lambda_expressions2.cpp // compile with: /EHsc /W4 #include #include int main() < using namespace std; int i = 3; int j = 5; // The following lambda expression captures i by value and // j by reference. functionf = [i, &j] < return i + j; >; // Change the values of i and j. i = 22; j = 44; // Call f and print its result. cout

В примере получается следующий результат.

Вызов лямбда-выражений

Можно вызывать лямбда-выражение сразу же, как показано в следующем фрагменте кода. Второй фрагмент кода показывает, как передать лямбда-код в качестве аргумента алгоритма стандартной библиотеки C++, find_if например.

Пример 1

В этом примере объявляется лямбда-выражение, которое возвращает сумму двух целых чисел и сразу же вызывает выражение с аргументами 5 и 4 .

// calling_lambda_expressions1.cpp // compile with: /EHsc #include int main() < using namespace std; int n = [] (int x, int y) < return x + y; >(5, 4); cout

В примере получается следующий результат.

Пример 2

В этом примере лямбда-выражение передается в качестве аргумента функции find_if . Лямбда-выражение возвращается true , если его параметр является четным числом.

// calling_lambda_expressions2.cpp // compile with: /EHsc /W4 #include #include #include int main() < using namespace std; // Create a list of integers with a few initial elements. listnumbers; numbers.push_back(13); numbers.push_back(17); numbers.push_back(42); numbers.push_back(46); numbers.push_back(99); // Use the find_if function and a lambda expression to find the // first even number in the list. const list::const_iterator result = find_if(numbers.begin(), numbers.end(),[](int n) < return (n % 2) == 0; >); // Print the result. if (result != numbers.end()) < cout else < cout > 

В примере получается следующий результат.

The first even number in the list is 42. 

Замечания

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

Вложенные лямбда-выражения

Пример

Можно вложить одно лямбда-выражение в другое, как показано в следующем примере. Внутреннее лямбда-выражение умножает его аргумент на 2 и возвращает результат. Внешнее лямбда-выражение вызывает внутреннее лямбда-выражение с использованием его аргумента и добавляет к результату 3.

// nesting_lambda_expressions.cpp // compile with: /EHsc /W4 #include int main() < using namespace std; // The following lambda expression contains a nested lambda // expression. int timestwoplusthree = [](int x) < return [](int y) < return y * 2; >(x) + 3; >(5); // Print the result. cout

В примере получается следующий результат.

Замечания

В этом примере значение параметра [](int y) < return y * 2; >является вложенным лямбда-выражением.

Лямбда-функции более высокого порядка

Пример

Многие языки программирования поддерживают концепцию функции более высокого порядка. Функция более высокого порядка — это лямбда-выражение, которое принимает другое лямбда-выражение в качестве аргумента или возвращает лямбда-выражение. Класс можно использовать function для включения лямбда-выражения C++ для поведения как функции более высокого порядка. В следующем примере показано лямбда-выражение, которое возвращает объект function , и лямбда-выражение, которое принимает объект function в качестве аргумента.

// higher_order_lambda_expression.cpp // compile with: /EHsc /W4 #include #include int main() < using namespace std; // The following code declares a lambda expression that returns // another lambda expression that adds two numbers. // The returned lambda expression captures parameter x by value. auto addtwointegers = [](int x) ->function < return [=](int y) < return x + y; >; >; // The following code declares a lambda expression that takes another // lambda expression as its argument. // The lambda expression applies the argument z to the function f // and multiplies by 2. auto higherorder = [](const function& f, int z) < return f(z) * 2; >; // Call the lambda expression that is bound to higherorder. auto answer = higherorder(addtwointegers(7), 8); // Print the result, which is (7+8)*2. cout

В примере получается следующий результат.

Использование лямбда-выражения в функции

Пример

Лямбда-выражения можно использовать в теле функции. Лямбда-выражение может получать доступ к любой функции или данным-членам, которые способна использовать включающая функция. Вы можете явно или неявно записать this указатель для предоставления доступа к функциям и элементам данных заключенного класса. Visual Studio 2017 версии 15.3 и более поздних версий (доступно с /std:c++17 и более поздними версиями): захват this по значению ( [*this] ) при использовании лямбда-лямбда-кода в асинхронных или параллельных операциях, где код может выполняться после выхода исходного объекта из область.

Указатель можно использовать this явно в функции, как показано ниже:

// capture "this" by reference void ApplyScale(const vector& v) const < for_each(v.begin(), v.end(), [this](int n) < cout ); > // capture "this" by value (Visual Studio 2017 version 15.3 and later) void ApplyScale2(const vector& v) const < for_each(v.begin(), v.end(), [*this](int n) < cout ); > 

Вы также можете записать this указатель неявно:

void ApplyScale(const vector& v) const < for_each(v.begin(), v.end(), [=](int n) < cout ); > 

В следующем примере показан класс Scale , который инкапсулирует значение масштаба.

// function_lambda_expression.cpp // compile with: /EHsc /W4 #include #include #include using namespace std; class Scale < public: // The constructor. explicit Scale(int scale) : _scale(scale) <>// Prints the product of each element in a vector object // and the scale value to the console. void ApplyScale(const vector& v) const < for_each(v.begin(), v.end(), [=](int n) < cout ); > private: int _scale; >; int main() < vectorvalues; values.push_back(1); values.push_back(2); values.push_back(3); values.push_back(4); // Create a Scale object that scales elements by 3 and apply // it to the vector object. Does not modify the vector. Scale s(3); s.ApplyScale(values); > 

В примере получается следующий результат.

3 6 9 12 

Замечания

Функция ApplyScale использует лямбда-выражение для выведения произведения масштаба на каждый элемент объекта vector . Лямбда-выражение неявно фиксирует this , чтобы он смог получить доступ к члену _scale .

Использование лямбда-выражений с шаблонами

Пример

Поскольку лямбда-выражения имеют тип, их можно использовать с шаблонами C++. В следующем примере показаны функции negate_all и print_all . Функция negate_all применяет унарный operator- к каждому элементу vector объекта. Функция print_all печатает каждый элемент в объекте vector в консоли.

// template_lambda_expression.cpp // compile with: /EHsc #include #include #include using namespace std; // Negates each element in the vector object. Assumes signed data type. template void negate_all(vector& v) < for_each(v.begin(), v.end(), [](T& n) < n = -n; >); > // Prints to the console each element in the vector object. template void print_all(const vector& v) < for_each(v.begin(), v.end(), [](const T& n) < cout ); > int main() < // Create a vector of signed integers with a few elements. vectorv; v.push_back(34); v.push_back(-43); v.push_back(56); print_all(v); negate_all(v); cout

В примере получается следующий результат.

34 -43 56 After negate_all(): -34 43 -56 

Замечания

Дополнительные сведения о шаблонах C++ см. в разделе "Шаблоны".

Обработка исключений

Пример

Тело лямбда-выражения выполняет правила как для структурированной обработки исключений (SEH), так и для обработки исключений C++. Можно обработать возникшее исключение в теле лямбда-выражения или перенести обработку исключения во включающий фрагмент. В следующем примере функция и лямбда-выражение используются for_each для заполнения vector объекта значениями другого. Он использует try / catch блок для обработки недопустимого доступа к первому вектору.

// eh_lambda_expression.cpp // compile with: /EHsc /W4 #include #include #include using namespace std; int main() < // Create a vector that contains 3 elements. vectorelements(3); // Create another vector that contains index values. vector indices(3); indices[0] = 0; indices[1] = -1; // This is not a valid subscript. It will trigger an exception. indices[2] = 2; // Use the values from the vector of index values to // fill the elements vector. This example uses a // try/catch block to handle invalid access to the // elements vector. try < for_each(indices.begin(), indices.end(), [&](int index) < elements.at(index) = index; >); > catch (const out_of_range& e) < cerr ; > 

В примере получается следующий результат.

Caught 'invalid vector subscript'. 

Замечания

Дополнительные сведения об обработке исключений см. в разделе "Обработка исключений".

Использование лямбда-выражений с управляемыми типами (C++/CLI)

Пример

Предложение захвата лямбда-выражения не может содержать переменную, которая имеет управляемый тип. Однако можно передать аргумент с управляемым типом в список параметров лямбда-выражения. В следующем примере содержится лямбда-выражение, которое захватывает локальную неуправляемую переменную ch по значению и принимает объект System.String в качестве параметра.

// managed_lambda_expression.cpp // compile with: /clr using namespace System; int main() < char ch = '!'; // a local unmanaged variable // The following lambda expression captures local variables // by value and takes a managed String object as its parameter. [=](String ^s) < Console::WriteLine(s + Convert::ToChar(ch)); >("Hello"); > 

В примере получается следующий результат.

Hello! 

Замечания

Можно также использовать лямбда-выражения с библиотекой STL/CLR. Дополнительные сведения см . в справочнике по библиотеке STL/CLR.

Лямбда-выражения не поддерживаются в этих управляемых сущностях среды CLR: ref class , ref struct и value class value struct .

Объяснение лямбда-выражений

Объяснение лямбда-выражений

У меня возникли вопросы о лямбда-выражениях и RxJava. Эти вопросы в основном касаются не полного понимания лямбда-выражений или RxJava. Я попытаюсь объяснить лямбда-выражения как можно проще. RxJava я опишу отдельно.

Лямбда-выражения и RxJava

Что такое лямбда-выражения? Лямбда-выражения – это «всего лишь» новый способ сделать то же самое, что мы всегда могли сделать, но в более чистом и менее многословном новом способе использования анонимных внутренних классов.

Анонимный внутренний класс в Java – это класс без имени, он должен использоваться, если вам необходимо переопределить методы класса или интерфейса. Анонимный внутренний класс может быть создан из класса или интерфейса.

abstract class Animal < abstract void speak(); >Animal a = new Animal() < void speak() < System.out.println("Woff"); >>;

В Android мы обычно используем анонимный внутренний класс в качестве слушателя, например, для кнопок такого рода:

Button btn = findViewById(R.id.button); btn.setOnClickListener( new View.OnClickListener() < @Override public void onClick(final View view) < // Do some fancy stuff with the view parameter. >> );

Вернемся к лямбда-выражениям. Это следующая часть, которая является частью предыдущего кода, который считается анонимным внутренним классом.

new View.OnClickListener() < @Override public void onClick(final View view) < // Do some fancy stuff with the view parameter. >>

Лямбда-выражения могут использоваться только в том случае, если вам нужно переопределить не более одного метода. К счастью для нас, View.OnClickListener содержит только один. Посмотрите на код ниже. Как думаете, какую его часть нам придётся убрать?

new View.OnClickListener() < @Override public void onClick(final View view) < // Do some fancy stuff with the view parameter. >>

После удаления практически всего кода, нам нужно добавить ->, как в приведенном ниже коде. Входящий параметр view может использоваться внутри функции так же, как и раньше.

(view) -> < // Do some fancy stuff with the view parameter. >

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

(View view) -> < // Do some fancy stuff with the view parameter. >

Вы также можете использовать многострочный код:

(view) ->

Если у вас есть интерфейс с методом, принимающим два параметра…

interface MyInterface

…лямбда-выражение будет выглядеть следующим образом:

Если метод имеет возвращаемый тип…

interface MySecondInterface

…лямбда-выражение будет выглядеть следующим образом:

Но это еще не все, есть некоторые специальные случаи, которые делают код еще меньше. Если тело вашего метода содержит только одну строку кода, вы можете удалить фигурные скобки <>. Если вы удаляете фигурные скобки, вам также необходимо удалить точку с запятой, оставив следующее:

(a, b) -> return a + b

Есть еще одна вещь, которую мы можем сделать. Если у нас только одна строка кода, то компилятор может понять, нужна ли возвращаемая часть или нет, поэтому мы можем оставить ее следующим образом:

(a, b) -> a + b

Если бы у нас был многострочный код, это свелось бы к следующему:

Так что же мы сделали? Мы взяли это:

new MySecondInterface() < @Override public int onSecondMethod(final int a, final int b) < return a + b; >>;

и превратили вот в это:

(a, b) -> a + b

Осталось только одна вещь, ссылки на методы. Допустим, у нас есть интерфейс, как и раньше, и метод, который этот интерфейс принимает как параметр:

public interface Callback < public void onEvent(int event); >public void myMethod(Callback callback)

Без лямбда-выражения это выглядело бы так:

myMethod(new Callback() < @Override public void onEvent(final int state) < System.out.println(state); >>); 

Добавив лямбда-выражение, как мы это делали раньше, получим следующее:

myMethod(state -> System.out.println(state));

Но это еще не все. Если используемый код – однострочный и вызываемая функция принимает один параметр, мы можем передавать ссылку на метод в таком виде:

myMethod(System.out::println);

Параметр будет передаваться автоматически, без необходимости использования другого кода! Удивительно, правда? Надеюсь, вы узнали что-то новое!

  • android development
  • android
  • java
  • rxJava
  • реактивное программирование
  • перевод с английского
  • программирование
  • разработка
  • devcolibri
  • андроид
  • джава
  • никто не читает теги
  • Программирование
  • Java
  • Разработка мобильных приложений
  • Разработка под Android

О разнице между лямбдами и обычными функциями и о имплементации лямбд в некторых языках программирования

Цель настоящей статьи - изучить лямбда функции: чем они отличаются от обычных функций и изучить, как они реализованы в С++, Python 3 и Java.

На протяжении этой статьи я буду использовать godbolt.org, чтобы компилировать код и изучать машинный код или байт код. Я думаю, что при чтении статьи может быть удобнее смотреть не на приведённый машинный код в статье, а на этом сайте.

C++

Что такое функция? Можно сказать, что функция - это набор выражений языка программирования, в данном случае С++, перед которым следует декларация функции из её названия, аргументов, типов и так далее. С точки зрения случая, когда вы передаёте одну функцию в другую, функция - это просто напросто указатель на начало функции в исполняемом файле. С точки зрения же машинного кода, функция - это последовательность инструкций, предварённая меткой с названием функции, такая, что эта последовательность соответствует calling conventions.

Окромя обыкновенных функция, в С++ есть и лямбда функции, которые можно объявлять внутри других функций. Как и обычные функции, они могут принимать аргументы и возвращать значения, но, в отличие от обычных функций, они могут "захватывать" значения из того контекста, в котором объявлены, по значению или по ссылке (иными словами, лямбда функция может использовать значения снаружи или указатели на них). Давайте посмотрим на несколько примеров с лямбдами на С++ и на то, в какой ассемблерный код они компилируются. Я буду использовать gcc 12.2 с флагом -Os (чем меньше ассемблерного кода будет сгенерировано, тем проще его читать).

#include #include // extern so that it won`t be inlined // функция объявлена как extern, чтобы её нельзя было заинлайнить extern void for_each(const std::vector& vec, void (*func) (int)); void example(const std::vector& vec) < for_each(vec, [] (int n) < printf("%d\n", n); >); >
.LC0: .string "%d\n" example(std::vector > const&)::::_FUN(int): movl %edi, %esi xorl %eax, %eax movl $.LC0, %edi jmp printf example(std::vector > const&): movl $example(std::vector > const&)::::_FUN(int), %esi jmp for_each(std::vector > const&, void (*)(int)) // note that using jmp instead of call is an optimization; the called // function will return to the callee of the current function, skipping `example`

В примере выше мы ничего особенного не видим: лямбда функция, которую мы объявили, просто напросто была скомпилирована как обычная функция (то есть, если мы бы объявили обычную функцию такого же типа и с таким же телом, ассемблер был бы такой же). Давайте посмотрим на более интересный пример, где лямбда захватит переменную снаружи.

#include #include extern void for_each(const std::vector& vec, void (*func) (int)); void example(const std::vector& vec, int threshold) < for_each(vec, [threshold] (int n) < if (n >threshold) < printf("%d\n", n); >>); >

Впрочем, это не скомпилируется:

: In function 'void example(const std::vector&, int)': :7:19: error: cannot convert 'example(const std::vector&, int)::' to 'void (*)(int)' 7 | for_each(vec, [threshold] (int n) < | ^~~~~~~~~~~~~~~~~~~~~ | | | example(const std::vector&, int):: 8 | if (n > threshold) < | ~~~~~~~~~~~~~~~~~~~~ 9 | printf("%d\n", n); | ~~~~~~~~~~~~~~~~~~ 10 | >| ~ 11 | >); | ~ :4:58: note: initializing argument 2 of 'void for_each(const std::vector&, void (*)(int))' 4 | extern void for_each(const std::vector& vec, void (*func) (int)); | ~~~~~~~^~~~~~~~~~~ Compiler returned: 1

Выясняется, что лямбда функция, объявленная внутри другой функции и захватывающая переменные снаружи, имеет более сложный тип, чем обычная функция, в данной случае, example(const std::vector&, int):: , то есть тип лямбда функции будет включать информацию о том, где она объявлена.

Из этого можно сделать два важных вывода. Во-первых, произвольная лямбда функция не может быть скомпилирована просто в ассемблерную функцию (как описано выше), это на самом деле объект, имеющий некоторое состояние. И во-вторых, лямбда функция, захватывающая аргументы снаружи, может быть передана только в шаблонную функцию, у которой тип принимаемой функции является шаблонным параметром (шаблонная функция - это шаблон функции, позволяющий компилятору генерировать функции для каждой подстановки параметров шаблона). Это означает, что для каждой новой лямбды, которую вы захотите передать в одну и ту же шаблонную функцию, компилятор будет вынужден сгенерировать новую версию шаблонной функции. Давайте посмотрим на пример:

#include #include template void __attribute__ ((noinline)) for_each(const std::vector& vec, Callable func) < for (const auto& elem : vec) < func(elem); >> void example(const std::vector& vec, int threshold) < for_each(vec, [threshold] (int n) < if (n >threshold) < printf("%d\n", n); >>); >
.LC0: .string "%d\n" void for_each  const&, int)::>(std::vector > const&, example(std::vector > const&, int)::): pushq %r12 pushq %rbp movl %esi, %ebp pushq %rbx movq 8(%rdi), %r12 movq (%rdi), %rbx .L2: cmpq %rbx, %r12 je .L7 movl (%rbx), %esi cmpl %ebp, %esi jle .L3 movl $.LC0, %edi xorl %eax, %eax call printf .L3: addq $4, %rbx jmp .L2 .L7: popq %rbx popq %rbp popq %r12 ret example(std::vector > const&, int): jmp void for_each  const&, int)::>(std::vector > const&, example(std::vector > const&, int)::)

Как можно видеть, реализация функции for_each была сгенерирована, и наша лямбда туда заинлайнилась - а for_each на самом деле оказалось функцией, принимающей вектор и число.

Это означает, что реализация лямбда функций в С++ основана на механизме шаблонов, и в большинстве случаев, лямбда функция будет инлайнится в шаблонную функцию.

Но не всегда! Мы (вроде) не может попросить компилятор не инлайнить лямбды, но мы можем сделать это дорогим. Поскольку у нас используется опция -Os , то компилятор должен постараться не инлайнить её в случае, если лямбда становится большой и передаётся сразу в несколько функций:

#include #include template void __attribute__ ((noinline)) for_each(const std::vector& vec, Callable func) < for (const auto& elem : vec) < func(elem); >> template void __attribute__ ((noinline)) for_each2(const std::vector& vec, Callable func) < for (auto iter = vec.rbegin(); iter != vec.rend(); iter++) < func(*iter); >> void example(const std::vector& vec, int threshold) < auto lambda = [threshold] (int n) < if (n >threshold) < printf("%d\n", n); printf("number: %d", n); printf("previous number: %d", --n); printf("next number: %d", n + 2); >>; for_each(vec, lambda); for_each2(vec, lambda); >
.LC0: .string "%d\n" .LC1: .string "number: %d" .LC2: .string "previous number: %d" .LC3: .string "next number: %d" example(std::vector > const&, int)::::operator()(int) const [clone .part.0]: pushq %rbx movl %edi, %esi movl %edi, %ebx xorl %eax, %eax movl $.LC0, %edi call printf movl %ebx, %esi movl $.LC1, %edi xorl %eax, %eax call printf leal -1(%rbx), %esi movl $.LC2, %edi xorl %eax, %eax call printf leal 1(%rbx), %esi movl $.LC3, %edi xorl %eax, %eax popq %rbx jmp printf void for_each  const&, int)::>(std::vector > const&, example(std::vector > const&, int)::): pushq %r12 pushq %rbp movl %esi, %ebp pushq %rbx movq 8(%rdi), %r12 movq (%rdi), %rbx .L4: cmpq %rbx, %r12 je .L9 movl (%rbx), %edi cmpl %ebp, %edi jle .L5 call example(std::vector > const&, int)::::operator()(int) const [clone .part.0] .L5: addq $4, %rbx jmp .L4 .L9: popq %rbx popq %rbp popq %r12 ret void for_each2  const&, int)::>(std::vector > const&, example(std::vector > const&, int)::): pushq %r12 movl %esi, %r12d pushq %rbp movq %rdi, %rbp pushq %rbx movq 8(%rdi), %rbx .L11: cmpq %rbx, 0(%rbp) je .L15 movl -4(%rbx), %edi cmpl %r12d, %edi jle .L12 call example(std::vector > const&, int)::::operator()(int) const [clone .part.0] .L12: subq $4, %rbx jmp .L11 .L15: popq %rbx popq %rbp popq %r12 ret example(std::vector > const&, int): subq $24, %rsp movl %esi, 12(%rsp) movq %rdi, (%rsp) call void for_each  const&, int)::>(std::vector > const&, example(std::vector > const&, int)::) movl 12(%rsp), %esi movq (%rsp), %rdi addq $24, %rsp jmp void for_each2  const&, int)::>(std::vector > const&, example(std::vector > const&, int)::)

Лямбда скомпилировалась в функцию, которая принимает число и печатает его несколько раз. Сама же проверка n > threshold заинлайнилась в функции for_each и for_each2 ! При этом, подстрока operator()(int) const в названии функции, соответствующей лямбда-функции, намекает на то, что компилятор представляет лямбда-функцию как структруру, которая может хранить то, что лямбда захватила и имеет оператор () [круглые скобки]. Ещё немного, и мы можем заставить компилятор ничего от лямбды не инлайнить:

#include #include extern bool predicate(int a, int threshold); template void __attribute__ ((noinline)) for_each(const std::vector& vec, Callable func) < for (const auto& elem : vec) < func(elem); >> template void __attribute__ ((noinline)) for_each2(const std::vector& vec, Callable func) < for (auto iter = vec.rbegin(); iter != vec.rend(); iter++) < func(*iter); >> void example(const std::vector& vec, int threshold) < auto lambda = [threshold] (int n) < if (predicate(n, threshold)) < printf("%d\n", n); printf("number: %d", n); printf("previous number: %d", --n); printf("next number: %d", n + 2); >>; for_each(vec, lambda); for_each2(vec, lambda); >
.LC0: .string "%d\n" .LC1: .string "number: %d" .LC2: .string "previous number: %d" .LC3: .string "next number: %d" // first argument in always passed in %edi, second in %esi, third in %edx example(std::vector > const&, int)::::operator()(int) const [clone .isra.0]: pushq %rbx movl %esi, %ebx movl %edi, %esi movl %ebx, %edi call predicate(int, int) testb %al, %al je .L1 movl %ebx, %esi movl $.LC0, %edi xorl %eax, %eax call printf movl %ebx, %esi movl $.LC1, %edi xorl %eax, %eax call printf leal -1(%rbx), %esi movl $.LC2, %edi xorl %eax, %eax call printf leal 1(%rbx), %esi movl $.LC3, %edi xorl %eax, %eax popq %rbx jmp printf .L1: popq %rbx ret void for_each2  const&, int)::>(std::vector > const&, example(std::vector > const&, int)::): pushq %r12 movl %esi, %r12d pushq %rbp movq %rdi, %rbp pushq %rbx movq 8(%rdi), %rbx .L6: cmpq %rbx, 0(%rbp) je .L9 movl -4(%rbx), %esi movl %r12d, %edi subq $4, %rbx call example(std::vector > const&, int)::::operator()(int) const [clone .isra.0] jmp .L6 .L9: popq %rbx popq %rbp popq %r12 ret void for_each  const&, int)::>(std::vector > const&, example(std::vector > const&, int)::): pushq %r12 pushq %rbp movl %esi, %ebp pushq %rbx movq 8(%rdi), %r12 movq (%rdi), %rbx .L11: cmpq %rbx, %r12 je .L14 movl (%rbx), %esi movl %ebp, %edi addq $4, %rbx call example(std::vector > const&, int)::::operator()(int) const [clone .isra.0] jmp .L11 .L14: popq %rbx popq %rbp popq %r12 ret example(std::vector > const&, int): subq $24, %rsp movl %esi, 12(%rsp) movq %rdi, (%rsp) call void for_each  const&, int)::>(std::vector > const&, example(std::vector > const&, int)::) movl 12(%rsp), %esi movq (%rsp), %rdi addq $24, %rsp jmp void for_each2  const&, int)::>(std::vector > const&, example(std::vector > const&, int)::)

В этот раз лямбда функция также является оператором круглые скобки, и она принимает первым аргументом (как бы внутри этой воображаемой структуры) threshold , а вторым - собственно - число из вектора. Аргументы меняются местами и передаются в predicate . Дальше всё понятно. Функции for_each , как и прежде, принимают вектор и threshold и передают последнее значение в лямбду.

Напследок, немного более хитрый пример:

#include #include template void __attribute__ ((noinline)) for_each(const std::vector& vec, Callable func) < for (const auto& elem : vec) < func(elem); >> auto get_filtering_lambda(int threshold) < return [threshold] (int n) < if (n >threshold) < printf("%d\n", n); >>; > void example(const std::vector& vec, int threshold)

Какой ассемблер мы бы здесь ожидали увидеть? Как мы помним, for_each на самом деле не принимает функций, а на самом деле принимает то, что лямбда захватывает. Это значит, что get_filtering_lambda просто должна предоставить эти самые захватываемые переменные:

.LC0: .string "%d\n" void for_each>(std::vector > const&, get_filtering_lambda(int)::): pushq %r12 pushq %rbp movl %esi, %ebp pushq %rbx movq 8(%rdi), %r12 movq (%rdi), %rbx .L2: cmpq %rbx, %r12 je .L7 movl (%rbx), %esi cmpl %ebp, %esi jle .L3 movl $.LC0, %edi xorl %eax, %eax call printf .L3: addq $4, %rbx jmp .L2 .L7: popq %rbx popq %rbp popq %r12 ret get_filtering_lambda(int): movl %edi, %eax ret example(std::vector > const&, int): jmp void for_each>(std::vector > const&, get_filtering_lambda(int)::)

Надо отметить, что реализация, опирающаяся на шаблоны - не единственная возможная. Можно было бы в большей степени полагаться на виртуальные классы. Компилятор мог бы переводить лямбда-функцию в функцию, принимающую захватываемые аргументы и аргументы лямбда функции (примерно как мы это уже видели выше, когда лямбда становилась оператором круглые скобки некоторой структуры). Потом эту лямбду можно запаковать вместе с захватыевамыми значениями в структуру, которую уже передать в нешаблонную функцию (эта функция должна принимать указатель на объект виртуального класса).

class IIntConsumer < public: virtual ~IIntConsumer() = 0; virtual void consume(int) = 0; >; void for_reach(const vector& vec, IIntConsumer* func) < for (const auto& elem : vec) < func->consume(elem); > >

Для лямбда функции, передаваемой в такую функцию, компилятор должен сгенерировать класс, имплементирующий IIntConsumer , затем создать объект такого класса и передать в функцию for_each .

void lambda_translation(int a, int captured0) < if (a >captured0) < printf("%d", a); >> class LambdaHolder: public IIntConsumer < public: LambdaHolder(int a, int b, int captured0) : captured0(captured0) <>void consume(int a) override < lambda_translation(a, captured0); >private: int captured0; >;

Преимущество такого подхода было бы в том, что имплементация функции for_each будет одинаковая для всех лямбда функций, что может сократить объём генерируемого машинного кода (что может быть снизить риск iTLB промахов) и время компеиляции. Однако вызов виртуальных методов дороже вызова нормальных функций - в то время как в случае с шаблонами функции будут инлайниться, и вызовов вообще не будет. Таким образом, в большинстве случаев производительность будет хуже.

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

Однако С++ - довольно гибкий язык, и в нём есть возможность обернуть функцию или лямбла функцию в некий объект, тип которого будет зависеть только от принимаемых и возвращаемых объектов функции, а не того, что она захватывает, где объявлена и так далее. А именно, это шаблонный класс std::function . Ипользовать его можно так:

void for_each(const vector &vec, function func) < for (const auto &elem: vec) < func(elem); >> int main() < int threshold = 1; auto func = function( [threshold](int n) < if (n >threshold) < printf("%d\n", n); >> ); vector vec = ; for_each(vec, func); >

То есть в С++ всё-таки можно оборачивать лямбда-функцию в некий объект и передавать его в нешаблонные функции. К сожалению, код function в STL выглядит так будто его декомпилировали а потом обфусцировали (декомпилировать это в принципе невозможно, шутка) не очень понятно, поэтому я написал упрощённую реализацию:

template class IFuncHolder < public: virtual RType operator()(Args . args) = 0; virtual ~IFuncHolder() = default; >; template class FuncHolderImpl : public IFuncHolder  < public: FuncHolderImpl(Callable&& callable) : callable(callable) <>~FuncHolderImpl() < >RType operator()(Args . args) override < return callable(args. ); >private: Callable callable; >; template class my_function; template class my_function  < public: templatemy_function(Callable&& f) < func = new FuncHolderImpl(std::forward(f)); > ~my_function() < delete func; >RType operator()(Args . args) < return func->operator()(args. ); > private: IFuncHolder *func; >; void for_each(const vector &vec, my_function func) < for (const auto &elem: vec) < func(elem); >>

Python

Давайте начнём с того, что напишем какой-нибудь код и посмотрим, в какой байт-код он компилируется. Я буду использовать Python 3.7. Вообще-то, в Питоне лямбда функции - это очень простые однострочные выражения, создающие функцию, которая не очень экспрессивна. Но в Питоне можно объявить одну функцию внутри другой, и внутренняя функция может захватывать переменные снаружи, как и лямбда функция в С++.

Но прежде чем читать байт код Питона, надо заметить три вещи. Во-первых, числа в левой колонке означают номера строк исходного кода (нумеруются с 1), которым соответствует этот блок байт-кода - таким образом, можно легко видеть, в какой байт-код транслируется каждая строка по отдельности. Во-вторых, байт-код Питона организован по-другому, чем машинный код, получающийся из С, С++ и так далее. В случае с машинным кодом, заголовок бинарного файла будет содержать адрес, откуда следует начать исполнение файла - это будет метка (функция) _start , которая объявлена в libstdc, которая уже дальше вызывает функцию main из вашего кода. В Питоне же байт код начинает выполняться с первой же инструкции - он импортирует другие модули, объявляет переменные и тому подобное. Поэтому вторая и первая строки функции окажутся в разных местах (не друг за другом) в итоговом байт-коде. И последнее, байт-код Питона исполняется стековой машиной, а машинный код - на машине с регистрами.

def func_generator(a): def inner(b): return a < b return inner print(func_generator(2)(3))
 1 0 LOAD_CONST 0 () 2 LOAD_CONST 1 ('func_generator') 4 MAKE_FUNCTION 0 6 STORE_NAME 0 (func_generator) 7 8 LOAD_NAME 1 (print) 10 LOAD_NAME 0 (func_generator) 12 LOAD_CONST 2 (2) 14 CALL_FUNCTION 1 16 LOAD_CONST 3 (3) 18 CALL_FUNCTION 1 20 CALL_FUNCTION 1 22 POP_TOP 24 LOAD_CONST 4 (None) 26 RETURN_VALUE Disassembly of : 2 0 LOAD_CLOSURE 0 (a) 2 BUILD_TUPLE 1 4 LOAD_CONST 1 () 6 LOAD_CONST 2 ('func_generator..inner') 8 MAKE_FUNCTION 8 10 STORE_FAST 1 (inner) 5 12 LOAD_FAST 1 (inner) 14 RETURN_VALUE Disassembly of : 3 0 LOAD_DEREF 0 (a) 2 LOAD_FAST 0 (b) 4 COMPARE_OP 0 (<) 6 RETURN_VALUE

Что же происходит в байт-коде выше? Сначала выполняется первая строка кода на Питоне: она кладёт на стек ссылку на байт-код собственно функции, название функции. Затем выполняется инструкция MAKE_FUNCTION , которая создаёт внутри интерпритатора Питона. В конце новосозданный объект сохраняется в глобальную переменную.

Далее исполняется седьмая строка из питоновского кода. Она кладёт на стек функции print и func_generator, аргумент к последней, вызывает её, затем кладёт на стек аргумент к следующей функции, вызывает её, затем вызывает print и возвращает None , чтобы следующий байт-код не выполнялся.

Когда выполняется func_generator , выполняется вторая линия питоновского кода: на стек загружается переменная a (инструкции LOAD_CLOSURE и LOAD_FAST - это одно и то же), создаёт кортеж из этой переменной, потом кладёт на стек ссылку на код функции inner и имя этой функции. Затем вызывается инструкция MAKE_FUNCTION - выглядит так, будто ей в данном случае передаётся кортеж из переменных, которые внутрення функция захватывает. Давайте посмотрим в документацию о том, что эта инструкция делает:

грубый перевод

Кладёт на стек новый объект функции. Снизу вверх стек должен состоять из следующих значений, в случае, если в переданном аргументе установлены соответствующие биты:

  • 0x1: кортеж значений по умолчанию для аргументов, кроме тех, которые только по ключу
  • 0x2: словарь значений по умолчанию для аргументов по ключу
  • 0x4: кортеж строк, составляющих аннотации для аргументов
  • 0x8: кортеж, состоящий из свободных переменных, то есть тех, которые лямбда захватывает.
  • код функции
  • имя функции

В конце концов, давайте посмотрим, что делает функция inner: она просто напросто загружает аргумент на стек, затем захваченное значение (из объекта функции) и сравнивает их.

В заключение, мы увидели, что:

  1. Функции в Питоне (все, а не только лямбды) - это не просто последовательности инструкций байт-кода, а объекты, содержащие некоторые данные.
  2. Поддержка по захвату переменных снаружи встроена прямо в виртуальную машину Питона.
  3. Программа на Питоне может сгенерировать произвольное количество функций.

Java

Теперь пришла пора посмотреть, что присходит в Java. Я буду использовать openjdk 19.

import java.util.ArrayList; import java.util.concurrent.atomic.*; class Example < public static void caller3(ArrayLista, AtomicInteger b, AtomicBoolean boo) < a.forEach((i) -> < if (i >b.get()) < System.out.println(i); boo.set(true); >else < b.decrementAndGet(); boo.set(false); >>); > >

Получится следующий байт-код:

class Example < public static void caller3(java.util.ArrayList, java.util.concurrent.atomic.AtomicInteger, java.util.concurrent.atomic.AtomicBoolean); 0: aload_0 1: aload_1 2: aload_2 3: invokedynamic #7, 0 // InvokeDynamic #0:accept:(Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicBoolean;)Ljava/util/function/Consumer; 8: invokevirtual #11 // Method java/util/ArrayList.forEach:(Ljava/util/function/Consumer;)V 11: return private static void lambda$caller3$0(java.util.concurrent.atomic.AtomicInteger, java.util.concurrent.atomic.AtomicBoolean, java.lang.Integer); 0: aload_2 1: invokevirtual #17 // Method java/lang/Integer.intValue:()I 4: aload_0 5: invokevirtual #23 // Method java/util/concurrent/atomic/AtomicInteger.get:()I 8: if_icmple 26 11: getstatic #28 // Field java/lang/System.out:Ljava/io/PrintStream; 14: aload_2 15: invokevirtual #34 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 18: aload_1 19: iconst_1 20: invokevirtual #40 // Method java/util/concurrent/atomic/AtomicBoolean.set:(Z)V 23: goto 36 26: aload_0 27: invokevirtual #46 // Method java/util/concurrent/atomic/AtomicInteger.decrementAndGet:()I 30: pop 31: aload_1 32: iconst_0 33: invokevirtual #40 // Method java/util/concurrent/atomic/AtomicBoolean.set:(Z)V 36: return >

Здесь можно увидеть, что, во-первых, лямбда функция стала статической функцией, которая принимает и переменные, которые лямбда захватывает снаружи, и собственные аргументы, и во вторых, что перед тем, как вызвать ArrayList::forEach , вызывается нечто, и в результате чего получается объект некоторого типа, который имплементирует Consumer . Сейчас не столь важно, что конкретно вызывается (это скорее тема для отдельной статьи, подробнее можно посмотреть здесь), а интересно, что за класс имплементирует вышеуказанный интерфейс. Этот класс невозможно найти среди генерируемых class файлов. Выясняется, что этот класс генерируется JVM в рантайме. Мы можем его увидеть, добавив флаг -jdk.internal.lambda.dumpProxyClasses при запуске JVM. После декомпиляции этого класса получим:

import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; // $FF: synthetic class final class Lambder$$Lambda$1 implements Consumer < private final AtomicInteger arg$1; private Lambder$$Lambda$1(AtomicInteger var1) < this.arg$1 = var1; >public void accept(Object var1) < Lambder.lambda$caller2$0(this.arg$1, (Integer)var1); >>

Итак, в Java происходит следующее:

  1. Для лямбда-функции, компилятор Java генерирует другую функцию, которая принимает и переменные, захватываемые снаружи, и аргументы собственно лямбда-функции.
  2. В рантайме JVM генерирует класс, который может хранить то, что лямбда захватывает снаружи и реализует нужный функциональный интерфейс, вызывая лямбда функцию с хранимыми значениями и аргументами.
  3. Объект этого сгенерированного класса создаётся и передаётся в функцию, которая принимает функциональный интерфейс.

Из этого видно, почему лямбда функции могут захватывать только final и effectively final переменные - она никак не может заменить объект, захваченный снаружи, на другой.

Обратите внимание, что то, что происходит в Java - это почти то же, что мы описывали выше как теоритический вариант, предложенный для С++, который не полагается так сильно на шаблоны - это неудивительно, так как в Java шаблонов нет.

Что такое лямбда-выражения?

@niki-timofe, это слабое оправдание. Если вы не приложите усилий, чтобы задать хороший вопрос, то не можете ожидать, что мы приложим усилия, чтобы дать хороший ответ. Таким образом вам придется удовлетвориться копипастой из ссылки выше, что несколько глупо для программиста, ибо подразумевает выполнение машинной работы вручную.

19 мая 2012 в 20:23

4 ответа 4

Сортировка: Сброс на вариант по умолчанию

Лямбда-функции - это функции, у которой фактически нет имени. Таким образом математики упростили до невозможности формат записи функции, а вообще лямбда-исчислениями пытались формализовать вычисления

λ — означает, что это лямбда-функция. Всё что после неё - список аргументов, в идеале абсолютно любого типа, в том числе может быть и другая лямбда-функция. После точки идёт "тело функции", а уже потом, собственно, идёт аргумент, который будет передан. Т.о.

λx.x+2 2 // вернёт 4 
λx.x 2 λy.y+1 // результат 3 

Здесь в качестве параметра x выступает другая лямбда-функция λy.y + 1 , в которую передаётся параметр 2. То есть любая лямбда функция является функцией высшего порядка, может принимать в качестве аргумента другую функцию и возвращать функцию:

λx.λy.y+x+3 2 // вернёт λy.y+5, т.к. x был равен двум. λx.λy.y+x+3 2 3 // вернёт 8. Фактически это каррирование: сначала функция принимает аргумент 2 и возвращает функцию, которая принимает ещё один аргумент и возвращает результат. 

Если интересно, я когда-то писал аналогичные вещи на C#

Теперь давайте посмотрим, как же все наши примеры будут выглядеть на C#. Здесь в качестве лямбда-функции я использую Func, где T - тип аргумента, а U - тип возвращаемого значения:

1) Func func = x=>x; 2) var result = new Func(x=>x+2)(2); 3) var result = new Func(x=>x(2))(new Func(y=>y+1)); 4) var result = new Func(x=>y=>y+x+3)(2); 5) var result = new Func(x=>y=>y+x+3)(2)(3); 

Сложность составляет только явное указание типа аргументов и возвращаемого значения.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *