Skip to content

Commit 6e2e39a

Browse files
SpyCheesesorokin
authored andcommitted
a section about lambdas
1 parent fad7051 commit 6e2e39a

File tree

3 files changed

+299
-1
lines changed

3 files changed

+299
-1
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
MAIN_FILE_BASE := main
2-
MAIN_FILE_DEPS := compilation.tex preprocessor.tex nullptr.tex rvalue-references.tex smart-pointers.tex
2+
MAIN_FILE_DEPS := compilation.tex preprocessor.tex nullptr.tex rvalue-references.tex smart-pointers.tex lambdas.tex
33
HOW_TO_BASE := how-to-contribute
44
TEX_CMD ?= pdflatex
55

lambdas.tex

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
\section{Анонимные функции}
2+
\subsection{Передача функции в качестве аргумента}
3+
Иногда требуется передать функцию в качестве аргумента в другую функцию.
4+
Пример такой ситуации~--- вызов \mintinline{c++}{std::sort} с нестандартным компаратором.
5+
6+
\mintinline{c++}{std::sort} принимает третьим аргументом объект, у которого определён \mintinline{c++}{operator ()}.
7+
Тип этого объекта задаётся аргументом шаблона. \mintinline{c++}{std::sort} вызывает этот оператор у объекта, чтобы произвести сравнение.
8+
9+
В качестве такого объекта может выступать указатель на функцию:
10+
\begin{minted} [linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
11+
bool cmp(int x, int y) {
12+
return x % 10 < y % 10;
13+
}
14+
std::vector<int> a;
15+
// ...
16+
std::sort(a.begin(), a.end(), cmp);
17+
\end{minted}
18+
19+
Внутри функции \mintinline{c++}{std::sort} \mintinline{c++}{comp} вызывается следующим образом:
20+
\begin{minted} [linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
21+
template <class Iterator, class Compare>
22+
void sort (Iterator first, Iterator last, Compare comp) {
23+
// ...
24+
if (comp(x, y)) {
25+
// ...
26+
}
27+
// ...
28+
}
29+
\end{minted}
30+
31+
\subsubsection{Функциональные объекты}
32+
Может потребоваться передать в передаваемую функцию какие-то дополнительные параметры.
33+
Например, нужно отсортировать числа, как в предыдущем примере, но вместо фиксированного модуля $10$
34+
требуется использовать некоторое значение, неизвестное при компиляции.
35+
\begin{minted} [linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
36+
bool cmp(int x, int y) {
37+
return x % k < y % k;
38+
}
39+
// ...
40+
k = 10;
41+
std::sort(a.begin(), a.end(), cmp);
42+
\end{minted}
43+
44+
Завести глобальную переменную~--- плохой вариант: создаётся лишняя глобальная переменная, которая используется только при вызове
45+
\mintinline{c++}{std::sort}, невозможно выполнять этот код многопоточно с разными значениями $k$.
46+
47+
Вместо этого можно передать объект, который содержит в себе нужную переменную и имеет \mintinline{c++}{operator ()}:
48+
\begin{minted} [linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
49+
struct Cmp {
50+
int k;
51+
Cmp(int nk): k(nk) {}
52+
bool operator () (int x, int y) const {
53+
return x % k < y % k;
54+
}
55+
};
56+
int k = 10;
57+
std::sort(a.begin(), a.end(), Cmp(k));
58+
\end{minted}
59+
60+
Если требуется изменять изнутри компаратора локальные переменные функции, вызывающей \mintinline{c++}{std::sort}, можно
61+
сохранить в этот объект ссылки на них.
62+
\begin{listing}
63+
\begin{minted} [linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
64+
struct Cmp {
65+
int k;
66+
int &cnt;
67+
Cmp(int nk, int &ncnt): k(nk), cnt(ncnt) {}
68+
bool operator () (int x, int y) const {
69+
++cnt;
70+
return x % k < y % k;
71+
}
72+
};
73+
int cnt = 0;
74+
int k = 10;
75+
std::sort(a.begin(), a.end(), Cmp(k, cnt));
76+
\end{minted}
77+
\caption{Функциональный объект-компаратор}
78+
\label{listing:functional_object_as_comparator}
79+
\end{listing}
80+
81+
Недостаток такого подхода~--- громоздскость. Приходится писать целый класс, у которого содержательная часть~--- только
82+
\mintinline{c++}{operator ()}. C++11 предоставляет способ создавать функциональные объекты меньшим количеством кода.
83+
84+
\subsection{Синтаксис лямбда-функций}
85+
Лямбда-функции (или анонимные функции)~--- это способ создавать функцинальные объекты в C++11.
86+
С применением лямбда-функции пример из листинга \ref{listing:functional_object_as_comparator} может быть реализован следующим образом:
87+
88+
\begin{minted} [linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
89+
int cnt = 0;
90+
int k = 10;
91+
std::sort(a.begin(), a.end(), [k, &cnt] (int x, int y) {
92+
++cnt;
93+
return x % k < y % k;
94+
});
95+
\end{minted}
96+
97+
Встретив такой код, компилятор создаёт анонимный класс, аналогичный классу \mintinline{c++}{Cmp} из листинга \ref{listing:functional_object_as_comparator},
98+
и передаёт его экземпляр в \mintinline{c++}{std::sort}.
99+
100+
Синтаксис лямбда-функции выглядит следующим образом:
101+
102+
\mintinline{text}{[capture-list] (params) specifiers exception-specification -> return-type { body }}
103+
104+
\begin{itemize}
105+
\item \emph{capture-list}~--- список переменных, которые будут доступны изнутри лямбды. Переменные могут передаваться
106+
по значению и по ссылке.
107+
\begin{itemize}
108+
\item \mintinline{c++}{[]}~--- пустой capture list.
109+
\item \mintinline{c++}{[a,b,c]}~--- захват переменных по значению.
110+
\item \mintinline{c++}{[&a,&b,&c]}~--- захват переменных по ссылке.
111+
\item \mintinline{c++}{[a,&b,c]}~--- захват по ссылке и по значению можно смешивать.
112+
\item \mintinline{c++}{[=]}~--- захват всех переменных, к которым обращается лямбда, по значению.
113+
\item \mintinline{c++}{[&]}~--- то же самое, но по ссылке.
114+
\item \mintinline{c++}{[&,a,b]}~--- захват $a$ и $b$ по значению, а всего остального~--- по ссылке.
115+
\item \mintinline{c++}{[=,&a,&b]}~--- захват $a$ и $b$ по ссылке, а всего остального~--- по значению.
116+
\item \mintinline{c++}{[this]}~--- захват \mintinline{c++}{this} той функции, в которая объявлена лямбда.
117+
\mintinline{c++}{[=]} и \mintinline{c++}{[&]} тоже захватывают \mintinline{c++}{this}.
118+
\item C++14: \mintinline{c++}{[x = 2 + 3]}~--- то же самое, что и захват по значению новой переменной $x$ с
119+
заданным значением. Это позволяет захватывать move-only типы: \mintinline{c++}{[x = std::move(x)]}
120+
\item C++14: \mintinline{c++}{[&x = a]}~--- захват ссылки на $a$ с именем $x$. Вместо $a$ может быть не только
121+
переменная, но и любая ссылка, например, на элемент массива.
122+
\end{itemize}
123+
Переменные, захваченные по значению, нельзя изменять изнутри лямбды, если не указан спецификатор \mintinline{c++}{mutable}.
124+
\item \emph{params}~--- список параметров функции.
125+
Значения параметров по умолчанию не разрешены до C++14.
126+
\item \emph{specifiers}:
127+
\begin{itemize}
128+
\item \mintinline{c++}{mutable}~--- разрешает изменять переменные, захваченные по значению, и вызывать у них
129+
не-const методы. Поскольку при захвате по значаению переменные копируются, их изменение не повлияет на значения
130+
переменных, которые были захвачены.
131+
\end{itemize}
132+
\item \emph{exception-specification}~--- можно указать спецификаторы \mintinline{c++}{throw}, \mintinline{c++}{noexcept},
133+
как у обычных функций.
134+
\item \emph{return-type}~--- тип возвращаемого значения. Если не указан, то тип будет выведен таким же образом,
135+
как и для функции, тип возвращаемого значения которой \mintinline{c++}{auto}~--- берётся тип выражения,
136+
которое используется в опереторе \mintinline{c++}{return}. Если их несколько, то они должны совпадать, а если их нет,
137+
то тип \mintinline{c++}{void}.
138+
Если тип возвращаемого значения не указан и функция не принимает никаких параметров, круглые скобки для параметров могут быть опущены.
139+
\end{itemize}
140+
141+
\subsection{Устройство функционального объекта}
142+
Для каждой лямбда-функции комплятор создаёт отдельный класс, называемый \emph{closure type}. Он содержит
143+
144+
\begin{itemize}
145+
\item \mintinline{c++}{operator ()} с заданными параметрами и типом возвращаемого значения.
146+
Если лямбда не имеет спецификатора \mintinline{c++}{mutable}, оператор является \mintinline{c++}{const}.
147+
\item Конструктор копирования и move-конструктор. Конструктор по умолчанию отсутствует.
148+
\item \mintinline{c++}{ClosureType& operator=(const ClosureType&) = delete;}
149+
\item Члены класса: захваченные по значению переменные, ссылки на захваченные по ссылке переменные.
150+
\item Если capture list пустой, то closure type может неявно приводиться к указателю на функцию с соответствующей сигнатурой.
151+
\end{itemize}
152+
153+
\subsection{Сохранения лямбда-функций в переменные}
154+
155+
Лямбда-функцию можно не только куда-то передать, но и сохранить в переменную или вернуть из функции.
156+
Поскольку тип лямбды не имеет имени, для объявления переменной или функции следует использовать \mintinline{c++}{auto}.
157+
158+
Обратите внимание, что захват переменных по ссылке не продлевает их время жизни. Например, следующий код
159+
будет работать неправильно:
160+
\begin{minted} [linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
161+
auto foo() {
162+
std::vector<int> v;
163+
return [&v] {
164+
// ...
165+
};
166+
}
167+
\end{minted}
168+
Вектор $v$ будет уничтожен при выходе из функции, поэтому обращение к нему из лямбды приведёт к
169+
неопределённому поведению. В этом случае необходимо захватывать по значению.
170+
171+
При копировании лямбды копируются все захваченные по значению переменные.
172+
173+
Поскольку каждая лямбда~--- отдельный тип, возникают определённые трудности:
174+
\begin{itemize}
175+
\item Нельзя завести переменную, которая может хранить лямбду не какого-то фиксированного типа, или завести массив разных лямбд.
176+
\item Нельзя вернуть из функции лямбду не фиксированного типа.
177+
\item Если нужно принять лямбду в функцию, приходится делать её шаблонной.
178+
\end{itemize}
179+
Те же проблемы есть и у обычных функциональных объектов. Их можно разрешить при помощи \mintinline{c++}{std::function}.
180+
181+
\subsubsection{std::function}
182+
183+
\mintinline{c++}{std::function}~--- это класс, принимающий в качестве парамеров шаблона типы аргументов и возвращаемого
184+
значения функции. В него можно записать любой объект с оператором \mintinline{c++}{()} с заданной сигнатурой:
185+
лямбду, обычный функциональный объект или указатель на функцию. Класс похож на \mintinline{c++}{std::any},
186+
но предназначен для хранения функциональных объектов и имеет \mintinline{c++}{operator ()}.
187+
188+
Поскольку функциональные объекты могут иметь разный размер, \mintinline{c++}{std::function} может выделять для них
189+
дополнительную память.
190+
191+
Поскольку \mintinline{c++}{std::function}~--- один тип для всех функцинальных объектов с заданной сигнатурой,
192+
в переменную такого типа можно сохранить любой подходящий функциональный объект, их можно без проблем
193+
складывать в коллекции или возвращать из функций.
194+
195+
\begin{minted} [linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
196+
#include <functional> // std::function определён в этом заголовочном файле
197+
198+
int bar(int a, int b) {
199+
return a * b;
200+
}
201+
202+
int main() {
203+
int n = 5;
204+
std::function<int(int, int)> foo = [n](int x, int y) {
205+
return x * n + y;
206+
};
207+
std::function<double(double)> foo2 = [n] (double x) { return n * x; };
208+
209+
foo = bar; // Можно хранить в std::function обычные указатели на функции
210+
211+
// std::function можно складывать в коллекции
212+
std::vector<std::function<double(double)>> fs = {cos, sin, foo2};
213+
std::cout << fs[0](0.1) << "\n";
214+
std::cout << fs[1](0.1) << "\n";
215+
std::cout << fs[2](0.1) << "\n";
216+
}
217+
\end{minted}
218+
219+
\mintinline{c++}{std::function} можно копировать. Неинициализированный \mintinline{c++}{std::function}
220+
не содержит никакого функционального объекта, при попытке вызова бросается \mintinline{c++}{std::bad_function_call}.
221+
Сделать \mintinline{c++}{std::function} пустым можно, присвоив ему \mintinline{c++}{nullptr},
222+
также с \mintinline{c++}{nullptr} можно сравнивать.
223+
224+
\subsubsection{Рекурсивный вызов лямбда-функции}
225+
Следующий код не скомпилируется:
226+
\begin{minted} [linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
227+
auto fact = [&fact] (int n) -> int {
228+
if (n == 0)
229+
return 1;
230+
return n * fact(n - 1);
231+
};
232+
std::cout << fact(5) << "\n";
233+
\end{minted}
234+
235+
Это происходит из-за того, что тип переменной \mintinline{c++}{fact} ещё не выведен в момент её использования.
236+
Чтобы можно было вызвать лямбду рекурсивно, нужно сохранить её в \mintinline{c++}{std::function}:
237+
\begin{minted} [linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
238+
std::function<int(int)> fact = [&fact] (int n) -> int {
239+
if (n == 0)
240+
return 1;
241+
return n * fact(n - 1);
242+
};
243+
std::cout << fact(5) << "\n";
244+
\end{minted}
245+
246+
Обратите внимание, что \mintinline{c++}{fact} захватывается по ссылке. В этом случае нельзя захватывать по значению, так
247+
ак в момент захвата переменной \mintinline{c++}{fact} ещё не присвоено значение.
248+
249+
Если нужно объявить несколько лямбда-функций, которые вызывают друг друга, нужно разделить объявление переменной и
250+
призваивание ей значения:
251+
\begin{minted} [linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
252+
std::function<int(int)> foo2;
253+
std::function<int(int)> foo1 = [&] (int n) -> int {
254+
if (n == 0)
255+
return 1;
256+
return n * foo2(n - 1);
257+
};
258+
foo2 = [&] (int n) -> int {
259+
if (n == 0)
260+
return 1;
261+
return 5 + foo1(n - 1);
262+
};
263+
std::cout << foo2(7) << "\n";
264+
\end{minted}
265+
266+
В этом примере \mintinline{c++}{foo1} можно сделать не \mintinline{c++}{std::function}, а просто \mintinline{c++}{auto},
267+
так как она не используется изнутри себя.
268+
269+
\subsection{Примеры}
270+
271+
\subsubsection{Поиск в глубину}
272+
\begin{minted} [linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
273+
std::vector<int> dfs(std::vector<std::vector<int>> const& e) {
274+
std::vector<int> tin(e.size(), -1);
275+
int t = 0;
276+
std::function<void(int)> go = [&] (int v) {
277+
if (tin[v] != -1)
278+
return;
279+
tin[v] = t++;
280+
for (int nv : e[v])
281+
go(nv);
282+
};
283+
go(0);
284+
return tin;
285+
}
286+
\end{minted}
287+
288+
\subsubsection{Упрощённый bind}
289+
\begin{minted} [linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
290+
template < typename Callable, typename... Ps >
291+
auto bind(Callable f, Ps... ps) {
292+
return [f, ps...] () {
293+
return f(ps...);
294+
};
295+
}
296+
\end{minted}
297+
Этот пример демонстрирует использование лямбд с variadic templates, а также возвращение лямбд из функций.

main.tex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@
4545
\include{nullptr}
4646
\include{rvalue-references}
4747
\include{smart-pointers}
48+
\include{lambdas}
4849

4950
\end{document}

0 commit comments

Comments
 (0)