Мой любимый инструмент в 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(){[](){}();}