Що таке лямбда-вираз у C ++ 11? Коли я використовую його? Який клас проблем вони вирішили, що було неможливо до їх введення?
Кілька прикладів та випадків використання було б корисно.
Що таке лямбда-вираз у C ++ 11? Коли я використовую його? Який клас проблем вони вирішили, що було неможливо до їх введення?
Кілька прикладів та випадків використання було б корисно.
Відповіді:
C ++ включає такі корисні загальні функції, як std::for_each
і std::transform
, що може бути дуже зручно. На жаль, вони також можуть бути досить громіздкими у використанні, особливо якщо функтор, який ви хочете застосувати, унікальний для певної функції.
#include <algorithm>
#include <vector>
namespace {
struct f {
void operator()(int) {
// do something
}
};
}
void func(std::vector<int>& v) {
f f;
std::for_each(v.begin(), v.end(), f);
}
Якщо ви використовуєте лише f
один раз і в цьому конкретному місці, здається, зайвим є написання цілого класу, щоб зробити щось тривіальне і одне.
У C ++ 03 ви можете спокуситись написати щось на зразок наступного, щоб зберегти функціонера локальним:
void func2(std::vector<int>& v) {
struct {
void operator()(int) {
// do something
}
} f;
std::for_each(v.begin(), v.end(), f);
}
однак це не дозволено, f
не може бути передано функції шаблону в C ++ 03.
C ++ 11 представляє лямбдаси, які дозволяють написати вбудований, анонімний функтор для заміни struct f
. Для невеликих простих прикладів це може бути більш чистим для читання (він зберігає все на одному місці) і потенційно простішим у підтримці, наприклад, у найпростішому вигляді:
void func3(std::vector<int>& v) {
std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}
Функції лямбда - це лише синтаксичний цукор для анонімних функціонерів.
У простих випадках для вас виводиться тип повернення лямбда, наприклад:
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) { return d < 0.00001 ? 0 : d; }
);
}
однак, коли ви почнете писати складніші лямбда, ви швидко зустрінете випадки, коли компілятор не може визначити тип повернення, наприклад:
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) {
if (d < 0.0001) {
return 0;
} else {
return d;
}
});
}
Щоб вирішити це рішення, ви можете чітко вказати тип повернення для функції лямбда, використовуючи -> T
:
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) -> double {
if (d < 0.0001) {
return 0;
} else {
return d;
}
});
}
Поки ми не використовували нічого, крім того, що було передано лямбда всередині нього, але ми також можемо використовувати й інші змінні в межах лямбда. Якщо ви хочете отримати доступ до інших змінних, ви можете використовувати пункт захоплення ( []
вираз), який до цих пір не використовувався в цих прикладах, наприклад:
void func5(std::vector<double>& v, const double& epsilon) {
std::transform(v.begin(), v.end(), v.begin(),
[epsilon](double d) -> double {
if (d < epsilon) {
return 0;
} else {
return d;
}
});
}
Ви можете захопити з допомогою як посилання і значення, яке можна задати з допомогою &
і =
відповідно:
[&epsilon]
захоплення посиланням[&]
фіксує всі змінні, використовувані в лямбда за посиланням[=]
фіксує всі значення, використовувані в лямбда за значенням[&, epsilon]
фіксує змінні типу [&], але epsilon за значенням[=, &epsilon]
фіксує змінні типу [=], але epsilon за посиланнямСформований operator()
це const
за замовчуванням, з підтекстом , що захоплює буде const
при зверненні до них за замовчуванням. Це призводить до того, що кожен виклик з одним і тим же входом дасть однаковий результат, однак ви можете відзначити лямбда якmutable
просити, щоб те, operator()
що виробляється, не було const
.
const
завжди ...
()
- він передається як лямбда-аргумент із нульовим аргументом, але, оскільки () const
не відповідає лямбда, він шукає конверсію типу, яка дозволяє це, що включає неявний припис -to-function-pointer, а потім викликає це! Підлий!
std::function<double(int, bool)> f = [](int a, bool b) -> double { ... };
Але зазвичай ми дозволяємо компілятору вивести тип: auto f = [](int a, bool b) -> double { ... };
(і не забудьте #include <functional>
)
return d < 0.00001 ? 0 : d;
гарантовано повертається подвійне, коли один з операндів є цілою константою (це через неявне правило просування оператора ?:, де 2-й і 3-й операнди врівноважуються один проти одного за допомогою звичайної арифметики конверсії незалежно від того, яку саме обрали). Якщо змінити 0.0 : d
, можливо, це полегшить розуміння прикладу.
Концепція лямбда-функції C ++ бере свій початок в обчисленні лямбда та функціональному програмуванні. Лямбда - це неназвана функція, яка корисна (у фактичному програмуванні, а не в теорії) для коротких фрагментів коду, які неможливо повторно використовувати і не варто називати.
У C ++ функція лямбда визначається так
[]() { } // barebone lambda
або у всій красі
[]() mutable -> T { } // T is the return type, still lacking throw()
[]
- це список захоплення, список ()
аргументів та {}
тіло функції.
Список захоплення визначає, що ззовні лямбда має бути доступним усередині функціонального тіла та як. Це може бути будь-який:
Ви можете змішати будь-що з перерахованого вище у списку, розділеному комами [x, &y]
.
Список аргументів такий же, як і в будь-якій іншій функції C ++.
Код, який буде виконуватися, коли фактично викликається лямбда.
Якщо лямбда має лише одне твердження повернення, тип повернення може бути пропущений і має неявний тип decltype(return_statement)
.
Якщо лямбда позначена мутаційною (наприклад []() mutable { }
), дозволяється мутувати значення, які були захоплені за значенням.
Бібліотека, визначена стандартом ISO, значною мірою користується лямбдами та підвищує зручність використання декількох барів, оскільки тепер користувачі не повинні захаращувати свій код невеликими функторами в деякій доступній області.
У С ++ 14 лямбд були розширені різними пропозиціями.
Елемент списку захоплення тепер може бути ініціалізований =
. Це дозволяє перейменувати змінні та фіксувати переміщенням. Приклад, взятий із стандарту:
int x = 4;
auto y = [&r = x, x = x+1]()->int {
r += 2;
return x+2;
}(); // Updates ::x to 6, and initializes y to 7.
та один із Вікіпедії, який показує, як зробити знімок за допомогою std::move
:
auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};
Лямбди тепер можуть бути загальними ( auto
було б рівнозначно T
тут, якби
T
аргумент шаблону типу був десь у навколишньому просторі):
auto lambda = [](auto x, auto y) {return x + y;};
C ++ 14 дозволяє виводити типи повернення для кожної функції і не обмежує її функціями форми return expression;
. Це поширюється і на лямбда.
r = &x; r += 2;
але це відбувається з початковим значенням 4.
Ламбда-вирази зазвичай використовуються для інкапсуляції алгоритмів, щоб вони могли бути передані іншій функції. Однак можна виконати лямбда відразу після визначення :
[&](){ ...your code... }(); // immediately executed lambda expression
функціонально еквівалентний
{ ...your code... } // simple code block
Це робить лямбда-вирази потужним інструментом для рефакторингу складних функцій . Ви починаєте, загортаючи розділ коду в лямбда-функцію, як показано вище. Процес явної параметризації може бути виконаний поступово з проміжним тестуванням після кожного кроку. Після того, як блок коду буде повністю параметризований (як показано видаленням &
), ви можете перемістити код у зовнішнє місце та зробити його нормальною функцією.
Так само ви можете використовувати лямбда-вирази для ініціалізації змінних на основі результату алгоритму ...
int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!
Як розділення логіки вашої програми , ви навіть можете вважати корисним передавати лямбда-вираз як аргумент іншому лямбда-виразу ...
[&]( std::function<void()> algorithm ) // wrapper section
{
...your wrapper code...
algorithm();
...your wrapper code...
}
([&]() // algorithm section
{
...your algorithm code...
});
Лямбда-вирази також дозволяють створювати названі вкладені функції , що може бути зручним способом уникнути дублювання логіки. Використання названих лямбдаз також має тенденцію бути дещо легшими для очей (порівняно з анонімними вбудованими лямбдатами) при передачі нетривіальної функції в якості параметра іншій функції. Примітка: не забудьте крапку з комою після закриття фігурної дужки.
auto algorithm = [&]( double x, double m, double b ) -> double
{
return m*x+b;
};
int a=algorithm(1,2,3), b=algorithm(4,5,6);
Якщо наступне профілювання виявить значну ініціалізацію накладних витрат для об'єкта функції, ви можете вибрати цю функцію як звичайну функцію.
if
висловлювань: if ([i]{ for (char j : i) if (!isspace(j)) return false ; return true ; }()) // i is all whitespace
припустимо, що i
цеstd::string
[](){}();
.
(lambda: None)()
Синтаксис Python настільки розбірливіший.
main() {{{{((([](){{}}())));}}}}
Відповіді
Питання: Що таке лямбда-вираз у C ++ 11?
A: Під капотом це об'єкт автогенерованого класу з const оператора перевантаження () . Такий об’єкт називається закриттям і створюється компілятором. Ця концепція "закриття" близька до концепції зв'язування з C ++ 11. Але лямбди зазвичай генерують кращий код. А дзвінки через закриття дозволяють повністю вбудовуватися.
Питання: Коли я використовую його?
A: Визначити "просту і малу логіку" і попросити компілятора виконати генерацію з попереднього питання. Ви даєте компілятору деякі вирази, якими ви хочете бути всередині оператора (). Усі інші компілятори речей генерують для вас.
Питання: Який клас проблем вони вирішили, що було неможливо до їх введення?
Відповідь: Це якийсь синтаксичний цукор, як оператори, що перевантажують замість функцій для спеціальних операцій додавання, підкорення ... Але це заощаджує більше рядків непотрібного коду, щоб зафіксувати 1-3 рядки реальної логіки для деяких класів тощо! Деякі інженери вважають, що якщо кількість рядків менша, то менше шансів помилитися в цьому (я теж думаю, що так)
Приклад використання
auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);
Екстра про лямбда, не охоплені питаннями. Ігноруйте цей розділ, якщо вас не цікавить
1. Захоплені цінності. Що ви можете захопити
1.1. Ви можете посилатися на змінну зі статичною тривалістю зберігання в лямбдах. Усі вони потрапляють у полон.
1.2. Ви можете використовувати лямбда для отримання значень "за значенням". У такому випадку захоплені vars будуть скопійовані в об’єкт функції (закриття).
[captureVar1,captureVar2](int arg1){}
1.3. Ви можете зафіксувати посилання. & - в цьому контексті означають посилання, а не покажчики.
[&captureVar1,&captureVar2](int arg1){}
1.4. Існує позначення для захоплення всіх нестатичних змін за значенням або за посиланням
[=](int arg1){} // capture all not-static vars by value
[&](int arg1){} // capture all not-static vars by reference
1.5. Існує позначення для захоплення всіх нестатичних змін за значенням або за посиланням та вказування smth. більше. Приклади: Захоплення всіх нестатичних vars за значенням, але шляхом відліку Param2
[=,&Param2](int arg1){}
Захоплення всіх нестатичних vars за посиланням, а за допомогою захоплення значення Param2
[&,Param2](int arg1){}
2. Відрахування типу повернення
2.1. Тип повернення лямбда можна вивести, якщо лямбда - це один вираз. Або ви можете чітко вказати це.
[=](int arg1)->trailing_return_type{return trailing_return_type();}
Якщо лямбда має більше одного виразу, то тип повернення повинен бути вказаний через зворотний тип повернення. Також подібний синтаксис може бути застосований до автоматичних функцій та функцій членів
3. Захоплені значення. Те, що ти не можеш захопити
3.1. Ви можете захоплювати лише локальні vars, а не змінні елементи об'єкта.
4. Конверсії
4.1 !! Лямбда не є покажчиком функції і не є анонімною функцією, але лямбдаси без захоплення можуть бути неявно перетворені у покажчик функції.
пс
Більше про інформацію про грамматику лямбда можна знайти в робочому проекті мови програмування C ++ # 337, 2012-01-16, 5.1.2. Лямбдаські вирази, с.88
У C ++ 14 додана додаткова функція, яка названа "init capture". Це дозволяє виконувати довільне оголошення членів даних про закриття:
auto toFloat = [](int value) { return float(value);};
auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};
[&,=Param2](int arg1){}
здається, не дійсні синтаксис. Правильна форма буде[&,Param2](int arg1){}
Функція лямбда - це анонімна функція, яку ви створюєте в режимі рядка. Він може фіксувати змінні, як пояснили деякі (наприклад, http://www.stroustrup.com/C++11FAQ.html#lambda ), але є деякі обмеження. Наприклад, якщо є такий інтерфейс зворотного виклику,
void apply(void (*f)(int)) {
f(10);
f(20);
f(30);
}
ви можете написати функцію на місці, щоб використовувати її, як та, яка передана для подання нижче:
int col=0;
void output() {
apply([](int data) {
cout << data << ((++col % 10) ? ' ' : '\n');
});
}
Але ви не можете цього зробити:
void output(int n) {
int col=0;
apply([&col,n](int data) {
cout << data << ((++col % 10) ? ' ' : '\n');
});
}
через обмеження в стандарті C ++ 11. Якщо ви хочете використовувати захоплення, вам потрібно покластися на бібліотеку та
#include <functional>
(або якусь іншу бібліотеку STL, наприклад алгоритм, щоб отримати це побічно), а потім працювати з std :: функцією замість передачі звичайних функцій як таких параметрів:
#include <functional>
void apply(std::function<void(int)> f) {
f(10);
f(20);
f(30);
}
void output(int width) {
int col;
apply([width,&col](int data) {
cout << data << ((++col % width) ? ' ' : '\n');
});
}
apply
шаблон, який прийняв функтор, він би спрацював
Один з кращих пояснення lambda expression
дається від автора C ++ Бьерн Страуструп в своїй книзі ***The C++ Programming Language***
главі 11 ( ISBN-13: 978-0321563842 ):
What is a lambda expression?
Лямбда - вираз , іноді також називають лямбда - функції або (строго кажучи , неправильно, але просторіччі) як лямбда , є спрощеною позначення для визначення і з допомогою анонімного об'єкта функції . Замість визначення названого класу з оператором (), згодом створення об'єкта цього класу і, нарешті, виклик до нього, ми можемо використовувати скорочення.
When would I use one?
Це особливо корисно, коли ми хочемо передати операцію як аргумент алгоритму. У контексті графічних інтерфейсів користувача (та інших) такі операції часто називають зворотними .
What class of problem do they solve that wasn't possible prior to their introduction?
Тут я здогадуюсь, що кожна дія, зроблена з лямбда-виразом, може бути вирішена без них, але з набагато більшим кодом та значно більшою складністю. Ламбда-вираз - це спосіб оптимізації вашого коду та спосіб зробити його більш привабливим. Як сумно від Stroustup:
ефективні способи оптимізації
Some examples
через лямбда-вираз
void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
for_each(begin(v),end(v),
[&os,m](int x) {
if (x%m==0) os << x << '\n';
});
}
або за допомогою функції
class Modulo_print {
ostream& os; // members to hold the capture list int m;
public:
Modulo_print(ostream& s, int mm) :os(s), m(mm) {}
void operator()(int x) const
{
if (x%m==0) os << x << '\n';
}
};
або навіть
void print_modulo(const vector<int>& v, ostream& os, int m)
// output v[i] to os if v[i]%m==0
{
class Modulo_print {
ostream& os; // members to hold the capture list
int m;
public:
Modulo_print (ostream& s, int mm) :os(s), m(mm) {}
void operator()(int x) const
{
if (x%m==0) os << x << '\n';
}
};
for_each(begin(v),end(v),Modulo_print{os,m});
}
якщо вам потрібно, ви можете назвати lambda expression
:
void print_modulo(const vector<int>& v, ostream& os, int m)
// output v[i] to os if v[i]%m==0
{
auto Modulo_print = [&os,m] (int x) { if (x%m==0) os << x << '\n'; };
for_each(begin(v),end(v),Modulo_print);
}
Або припустимо інший простий зразок
void TestFunctions::simpleLambda() {
bool sensitive = true;
std::vector<int> v = std::vector<int>({1,33,3,4,5,6,7});
sort(v.begin(),v.end(),
[sensitive](int x, int y) {
printf("\n%i\n", x < y);
return sensitive ? x < y : abs(x) < abs(y);
});
printf("sorted");
for_each(v.begin(), v.end(),
[](int x) {
printf("x - %i;", x);
}
);
}
буде генерувати далі
0
1
0
1
0
1
0
1
0
1
0 сортинг - 1; х - 3; х - 4; х - 5; х - 6; х - 7; х - 33;
[]
- це список захоплення або lambda introducer
: якщо lambdas
не потребує доступу до їх локального середовища, ми можемо ним скористатися.
Цитата з книги:
Перший символ лямбдаського виразу завжди [ . Вступник лямбда може приймати різні форми:
• [] : порожній список захоплення. Це означає, що в тілі лямбда не можна використовувати локальні назви з навколишнього контексту. Для таких лямбда-виразів дані отримують з аргументів або з нелокальних змінних.
• [&] : неявна фіксація шляхом посилання. Можна використовувати всі локальні назви. До всіх локальних змінних можна отримати посилання.
• [=] : неявна фіксація за значенням. Можна використовувати всі локальні назви. Усі імена відносяться до копій локальних змінних, взятих у точці виклику лямбда-виразу.
• [список захоплення]: явне захоплення; список захоплення - це список імен локальних змінних, які підлягають захопленню (тобто зберігаються в об'єкті) за посиланням або за значенням. Змінні з іменами, які передують &, фіксуються за посиланням. Інші змінні фіксуються за значенням. Список захоплення також може містити це та імена, за якими слідує ... як елементи.
• [&, capture-list] : неявно фіксує за допомогою посилання всі локальні змінні з іменами, не зазначеними у списку. Список захоплення може містити це. Перелічені імена не можуть передувати &. Змінні, названі у списку захоплення, фіксуються за значенням.
• [=, улаштування списку] : неявно фіксує за значенням всі локальні змінні з іменами, не згаданими у списку. Список захоплення не може містити це. Переліченим іменам має передувати &. Змінні, названі у списку захоплення, фіксуються за посиланням.
Зауважте, що локальне ім'я, яке передує &, завжди фіксується посиланням, а локальне ім'я, яке не передує &, завжди захоплюється за значенням. Тільки захоплення за посиланням дозволяє змінювати змінні в середовищі виклику.
Additional
Lambda expression
формат
Додаткові посилання:
for (int x : v) { if (x % m == 0) os << x << '\n';}
Ну, я знайшов одне практичне використання - зменшити код пластини котла. Наприклад:
void process_z_vec(vector<int>& vec)
{
auto print_2d = [](const vector<int>& board, int bsize)
{
for(int i = 0; i<bsize; i++)
{
for(int j=0; j<bsize; j++)
{
cout << board[bsize*i+j] << " ";
}
cout << "\n";
}
};
// Do sth with the vec.
print_2d(vec,x_size);
// Do sth else with the vec.
print_2d(vec,y_size);
//...
}
Без лямбда вам може знадобитися щось зробити для різних bsize
випадків. Звичайно, ви можете створити функцію, але що робити, якщо ви хочете обмежити використання в межах функції користувача користувача soul? природа лямбда відповідає цій вимозі, і я використовую її для цього випадку.
Лямбда в c ++ трактується як функція "доступна в дорозі". так, це буквально на ходу, ви визначаєте це; використай це; і коли закінчується область батьківської функції, лямбда-функція зникла.
c ++ представив його в c ++ 11, і всі почали використовувати його як у будь-якому можливому місці. Приклад і що таке лямбда можна знайти тут https://en.cppreference.com/w/cpp/language/lambda
Я опишу те, чого немає, але важливо знати кожному програмісту на C ++
Ламбда не призначена для використання скрізь, і кожна функція не може бути замінена лямбда. Це також не найшвидше порівняння зі звичайною функцією. тому що у нього є накладні витрати, з якими потрібно обробляти лямбда.
це, безумовно, допоможе зменшити кількість рядків у деяких випадках. він в основному може бути використаний для розділу коду, який викликається в одній і тій же функції один раз або кілька разів, і цей фрагмент коду більше не потрібен ніде, щоб ви могли створити для нього окрему функцію.
Нижче наведено основний приклад лямбда та те, що відбувається на задньому плані.
Код користувача:
int main()
{
// Lambda & auto
int member=10;
auto endGame = [=](int a, int b){ return a+b+member;};
endGame(4,5);
return 0;
}
Як компілювати розширює його:
int main()
{
int member = 10;
class __lambda_6_18
{
int member;
public:
inline /*constexpr */ int operator()(int a, int b) const
{
return a + b + member;
}
public: __lambda_6_18(int _member)
: member{_member}
{}
};
__lambda_6_18 endGame = __lambda_6_18{member};
endGame.operator()(4, 5);
return 0;
}
тож, як ви бачите, який тип накладних витрат додається, коли ви його використовуєте. тому не дуже гарно їх використовувати всюди. його можна використовувати в місцях, де вони застосовні.
Одна проблема, яку вона вирішує: Код простіший за лямбда для виклику в конструкторі, який використовує функцію вихідного параметра для ініціалізації члена const
Ви можете ініціалізувати члена const свого класу з викликом функції, яка встановлює його значення, повертаючи його вихід у якості вихідного параметра.