|
| 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, а также возвращение лямбд из функций. |
0 commit comments