C++17 STL Стандартная библиотека шаблонов — страница 34 из 119


 Если вы попытаетесь захватить переменную-член некоторого объекта, то не сможете сделать это с помощью конструкции

[member_a]
()
{...}
. Вместо этого нужно определить либо
this
, либо
*this
.


mutable (необязательный)

Если объект функции должен иметь возможность модифицировать получаемые им переменные путем копирования (

[=]
), то его следует определить как
mutable
. Это же касается вызова неконстантных методов захваченных объектов.


constexpr (необязательный)

Если мы явно пометим лямбда-выражение с помощью ключевого слова

constexpr
, то компилятор сгенерирует ошибку, когда это выражение не будет соответствовать критериям функции
constexpr
. Преимущество использования функций
constexpr
и лямбда-выражений заключается в том, что компилятор может оценить их результат во время компиляции, если они вызываются с параметрами, постоянными на протяжении данного процесса. Это приведет к тому, что позднее в бинарном файле будет меньше кода.

Если мы не указываем явно, что лямбда-выражения являются

constexpr
, но эти выражения соответствуют всем требуемым критериям, то они все равно будут считаться
constexpr
, только неявно. Если нужно, чтобы лямбда-выражение было
constexpr
, то лучше явно задавать его таковым, поскольку иначе в случае наших неверных действий компилятор начнет генерировать ошибки.


exception attr (необязательный)

Здесь определяется, может ли объект функции генерировать исключения, если при вызове столкнется с ошибкой.


return type (необязательный)

При необходимости иметь полный контроль над возвращаемым типом, вероятно, не нужно, чтобы компилятор определял его автоматически. В таких случаях можно просто использовать конструкцию

[] () -> Foo {}
, которая укажет компилятору, что мы всегда будем возвращать объекты типа
Foo

Добавляем полиморфизм путем оборачивания лямбда-выражений в std::function

 Предположим, нужно написать функцию-наблюдатель для какого-то значения, которое может изменяться время от времени, что приведет к оповещению других объектов, например индикатора давления газа, цены на акцию т.п. При изменении значения должен вызываться список объектов-наблюдателей, которые затем по-своему на это отреагируют.

Для реализации задачи можно поместить несколько объектов функции-наблюдателя в вектор, все они будут принимать в качестве параметра переменную типа

int
, которая представляет наблюдаемое значение. Мы не знаем, что именно станут делать данные функции при вызове, но нам это и неинтересно.

Какой тип будут иметь объекты функций, помещенные в вектор? Нам подойдет тип

std::vector
, если мы захватываем указатели на функции, имеющие сигнатуры наподобие
void f(int);
. Данный тип сработает с любым лямбда-выражением, которое захватывает нечто, имеющее совершенно другой тип в сравнении с обычной функцией, поскольку это не просто указатель на функцию, а объект, объединяющий некий объем данных с функцией! Подумайте о временах до появления С++11, когда лямбда-выражений не существовало. Классы и структуры были естественным способом связывания данных с функциями, и при изменении типов членов класса получится совершенно другой класс. Это естественно, что вектор не может хранить значения разных типов, используя одно имя типа.

Не стоит указывать пользователю, что он может сохранить объекты функции наблюдателя, которые ничего не захватывают, поскольку это ограничивает варианты применения. Как же позволить ему сохранять любые объекты функций, ограничивая лишь интерфейс вызова, принимающий конкретный диапазон параметров в виде наблюдаемых значений?

В этом разделе мы рассмотрим способ решения данной проблемы с помощью объекта

std::function
, который может выступать в роли полиморфической оболочки для любого лямбда-выражения, независимо от того, какие значения оно захватывает.



Как это делается

В этом примере мы создадим несколько лямбда-выражений, значительно отличающихся друг от друга, но имеющих одинаковую сигнатуру вызова. Затем сохраним их в одном векторе с помощью

std::function
.


1. Сначала включим необходимые заголовочные файлы:


#include 

#include 

#include 

#include 

#include 


2. Реализуем небольшую функцию, которая возвращает лямбда-выражение. Она принимает контейнер и возвращает объект функции, захватывающий этот контейнер по ссылке. Сам по себе объект функции принимает целочисленный параметр. Когда данный объект получает целое число, он добавит его в свой контейнер.


static auto consumer (auto &container){

  return [&] (auto value) {

    container.push_back(value);

  };

}


3. Еще одна небольшая вспомогательная функция выведет на экран содержимое экземпляра контейнера, который мы предоставим в качестве параметра:


static void print (const auto &c)

{

  for (auto i : c) {

    std::cout << i << ", ";

  }

  std::cout << '\n';

}


4. В функции

main
мы создадим объекты классов
deque
,
list
и
vector
, каждый из которых будет хранить целые числа:


int main()

{

  std::deque d;

  std::list l;

  std::vector v;


5. Сейчас воспользуемся функцией consumer для работы с нашими экземплярами контейнеров

d
,
l
и
v:
создадим для них объекты-потребители функций и поместим их в экземпляр
vector
. Эти объекты функций будут захватывать ссылку на один из объектов контейнера. Последние имеют разные типы, как и объекты функций. Тем не менее вектор хранит экземпляры типа
std::function
. Все объекты функций неявно оборачиваются в объекты типа
std::function
, которые затем сохраняются в векторе:


  const std::vector> consumers

    {consumer(d), consumer(l), consumer(v)};


6. Теперь поместим десять целочисленных значений во все структуры данных, проходя по значениям в цикле, а затем пройдем в цикле по объектам функций-потребителей, которые вызовем с записанными значениями:


  for (size_t i {0}; i < 10; ++i) {

    for (auto &&consume : consumers) {

      consume(i);

    }

  }


7. Все три контейнера теперь должны содержать одинаковые десять чисел. Выведем на экран их содержимое:


  print(d);

  print(l);

  print(v);

}


8. Компиляция и запуск программы дадут следующий результат, который выглядит именно так, как мы и ожидали:


$ ./std_function

0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

0, 1, 2, 3, 4, 5, 6, 7, 8, 9,


Как это работает

Самой сложной частью этого примера является следующая строка:


const std::vector> consumers

  {consumer(d), consumer(l), consumer(v)};


Объекты

d
,
l
и
v
обернуты в вызов
consumer(...)
. Он возвращает объекты функций, каждый из которых захватывает ссылки на один из объектов —
d
,
l
или
v
. Хотя все объекты функций принимают в качестве параметров целочисленные значения, тот факт, что они захватывают абсолютно