Мой любимый инструмент в C++ - это лямбда функции, хотя как-то мне говорили, что они кажутся страшными. На самом деле они прелестны. Они значительно упрощают написание программ и позволяют делать довольно интересные решения.
Но прежде чем рассматривать различные способы применения лямбда функций, предлагаю ознакомиться с основным синтаксисом лямбда функций.
Возможные варианты синтаксиса лямбда функций
[ capture ] ( params ) mutable exception attribute -> ret { body } [ capture ] ( params ) -> ret { body } [ capture ] ( params ) { body } [ capture ] { body }
Первый вариант является полным, но не запрещается использовать сокращённые вариации записи функций.
- capture - список внешних захватываемых объектов, они могут захватываться как по ссылке, так и копированием.
- params - список параметров, передаваемых в лямбда функции, данная часть будет аналогична записи аргументов для обычных функций.
- mutable - использование mutable позволяет модифицировать копии объектов, которые были захвачены копированием. В обычном варианте они не будут модифицироваться.
- exception - обеспечивает спецификацию исключения, то есть лямбда функции также как и обычные функции могут выкидывать исключения.
- attribute - обеспечивает спецификацию атрибута, таких атрибутов в спецификации C++ определено всего два ([[noreturn]], [[carries_dependency]])
- params - список параметров, передаваемых в лямбда функцию
- ret - возвращаемое значение лямбда функции
Что касается возвращаемого значение, то оно может автоматически выводиться из типа объекта, который возвращается оператором return. Если же в лямбда-функции отсутствует оператор return, то возвращаемое значение будет void.
Лямбда функция создаёт безымянный временный объект уникального безымянного non-union, non-aggregate типа, известного как тип замыкания. Благодаря введению оператора auto в современном стандарте C++ можно объявить объект лямбда функции довольно легко, без прописывания объявления функтора ( std::function ) со всеми апраметрами и возвращаемыми значениями, что делает код более простым и читаемым (для опытного программиста, конечно. Безусловно нужно учитывать то, что новичок быстрее заподозрит неладное, если в объявлении лямбды будет фигурировать std::function, но это уже вопрос практики).
Вот пример объявления простой лямбда функции, которая будет возвращать тип void , поскольку отсутствует хотя бы один оператор return .
#include <iostream> using namespace std; int main() { auto myLambda = []() { cout << "Hello World!" << endl; }; myLambda(); return 0; }
Соответственно программный код не скомпилируется, если в лямда-функции будет присутствовать два и более оператора return, которые будут возвращать объекты разных типов, не связанных между собой иерархией наследования и не способные быть приведены к типу базового класса. И даже, если эти объекты имеют базовый класс, необходимо будет прописать тип возвращаемого значения, им как раз будет указатель на объект базового класса (в общем случае).
Вот пример кода, который не скомпилируется.
#include <iostream> using namespace std; class A { public: A(){} }; class B : public A { public: B(){} }; class C : public A { public: C(){} }; int main() { auto myLambda = [](int type) { if (type == 0) { return new B(); } else { return new C(); } }; myLambda(0); return 0; }
Нужно указать тип возвращаемого значения
auto myLambda = [](int type) -> A* // Укажем тип возвращаемого значения { if (type == 0) { return new B(); } else { return new C(); } };
Также ошибка компиляции будет в том случае, если не указать тип возвращаемого значения и при этом вы создаёте в куче объект внутри лямбда функции, но в некоторых случаях можете вернуть указатель на nullptr. То есть ниже следующий код не скомпилируется.
#include <iostream> using namespace std; class A { public: A(){} }; int main() { auto myLambda = [](int type) { if (type == 0) { return new A(); } else { return nullptr; } }; myLambda(0); return 0; }
Опять нужно указать тип возвращаемого значения
auto myLambda = [](int type) -> A* // Укажем тип возвращаемого значения { if (type == 0) { return new A(); } else { return nullptr; } };
Дело в том, что nullptr - это универсальный тип данных, который в каком-то смысле не является типом данных, поскольку его нельзя установить в качестве типа переменной. Но он может быть присвоен в качестве значения указателю на объект. Чтобы неявное преобразование в данном случае происходило правильно, нужно также указать тип возвращаемого значения.
Также в выше приведённом примере показано, как вызвать лямда функцию и передать в неё параметры. Заметили? В данном примере используется параметр int type , в зависимости от которого мы возвращаем указатель на созданный объект или nullptr .
Также в лямбда функциях присутствует понятие захвата переменных. Это означает, что лямбда функция может использовать не только переменные, которые передаются ей в качестве параметров, но и какие-либо объекты, которые были объявлены вне лямда-функции.
Список символов может быть передан следующим образом:
**[a,&b]**
где a захвачена по значению, а b захвачена по ссылке.**[this]**
захватывает указатель**this**
по значению.**[&]**
захват всех символов по ссылке**[=]**
захват всех символов по значению**[]**
ничего не захватывает
Про захват переменных поговорим в следующих статьях.
Но отмечу один интересный момент, лямда-функцию можно вызвать сразу же там, где вы её и объявили, если добавить после тела лямда функции круглые скобки и передать все необходимые параметры, если они имеются.
Например такой код тоже скомпилируется
#include <iostream> using namespace std; class A { public: A(){} }; int main() { A* myA = [](int type) -> A* { if (type == 0) { return new A(); } else { return nullptr; } }(0); return 0; }
Так что подумайте, скомпилируется ли следующий программный код?
int main(){[](){}();}