Функторы и лямбда-выражения.


Функторами называют классы, в которых переопределена операция-функция operator(). Например, ниже приведен пример такого класса.

#include "stdafx.h"

#include <iostream>

#include <string>

 

class SimpleFunctor {

std::string name_;

public:

SimpleFunctor() : name_("") {}

SimpleFunctor(const char *name) : name_(name) {}

void operator()(const char *lastname) { std::cout << "Oh, hello, " << name_ <<" "<< lastname << endl; }

void operator()() { std::cout << "Oh, goodbye, " << name_ << endl; }

};

 

int main() {

SimpleFunctor sf("Mark");

//явные вызовы операций-функций operator()(const char *lastname) и operator()()

sf.operator()("Smirnov"); // выводит "Oh, hello, Mark Smirnov"

sf.operator()(); // выводит "Oh, goodbye, Mark"

//неявные вызовы операций-функций operator()(const char *lastname) и operator()()

sf("Smirnov"); // выводит "Oh, hello, Mark Smirnov"

sf(); // выводит "Oh, goodbye, Mark"

//создание неименованных объектов SimpleFunctor и неявный вызов операции-функции operator()

SimpleFunctor("Stiven")("Krasnov"); //вызывается конструктор с одним параметром и operator()(const char *lastname)

SimpleFunctor("Stiven")(); //вызывается конструктор с одним параметром и operator()()

SimpleFunctor()(); //вызывается конструктор без параметров и operator()()

}

Как видно из листинга программы в классе объявлены даже две операции-функции operator() c разными формальными параметрами, следовательно, объекты класса SimpleFunctor можно называть функторами. Классы – функторы получили своё эксклюзивное имя, так как они играют важную роль при использовании алгоритмов библиотеки STL. С одной стороны это обычные объекты, но с другой стороны, они похожи на функции, которые могут сохранять своё предыдущее состояние.

Например, алгоритм for_each библиотеки STL выполняет действие над элементами контейнера, но для этого ему необходимо передать три параметра: итераторы, настроенные на первый и последний элементы над которыми будут выполняться действие, и указатель на функцию или функтор, которые реализуют само это действие. Рассмотрим листинг программы, приведённый ниже.

#include "stdafx.h"

#include <algorithm>

#include <iostream>

#include <vector>

 

using namespace std;

 

class MyFunctor

{

MyFunctor(){}

public: void operator ()(int& _x) const { _x = _x*_x; }

private:

 

};

 

void print(int _x) { cout << _x << " "; }

 

void main()

{

vector<int> srcVec;

for (int val = 0; val < 10; val++)

srcVec.push_back(val);

for_each(srcVec.begin(), srcVec.end(), MyFunctor());

cout << endl;

for_each(srcVec.begin(), srcVec.end(), print);

cout << endl;

}

Рассмотрим листинг программы, приведённый выше. В функции main создаётся вектор srcVec элементов типа int. В цикле вектор srcVec заполняется элементами от 0 до 9. Далее с помощью алгоритма for_each над каждым элементом ветора производится действие, реализованное в операции-функции operator() класса MyFunctor (в данном случае это возведение в квадрат каждого элемента вектора). С помощью следующего вызова алгоритма for_each и указателя на функцию print мы выводим значения вектора на экран. Таким образом, в алгоритмах STL для реализации какого-либо действия над элементами каонтейнера можно использовать либо функтор, либо указатель на функцию.

В языке С++ есть ещё один способ реализации действия над элементами контейнера – это лямбда выражения. Лямбда-выражение это фактически краткая запись неименнованного функтора. Общий вид лямбда-выражения следующий:

lambda-expression ::=

‘[’ [список_захвата] ‘]’

[ ‘(’ список_параметров ‘)’ [mutable] ]

[noexcept]

[-> тип_возвращаемого_значения ]

‘{’ [тело_лямбды] ‘}’

где список_захвата - это с одной стороны список фактических параметров, которые передаются в конструктор неименованного функтора, а с другой стороны, на основании этого списка в функторе объявляются соответствующего типа (и с аналогичными идентификаторами) компонентные данные, которые инициализируются переменными из списка_захвата; на основании всех остальных параметров формируется оперция-функция operator() неименованного функтора: [тип_возвращаемого_значения] operator()([список_параметров]) [mutable] [noexcept] { [тело_лямбды] }; в квадратные скобки [] заключены параметры, которые можно опустить; в одиночные апострофы ‘’ заключены синтаксические элементы, которые опускать нельзя; если параметр mutable опущет, то поумолчанию используется const.

Рассмотрим листинг программы приведённый ниже.

#include "stdafx.h"

#include <algorithm>

#include <iostream>

#include <vector>

 

using namespace std;

 

void main(){

vector<int> srcVec;

for (int val = 0; val < 10; val++)

srcVec.push_back(val);

for_each(srcVec.begin(), srcVec.end(), [](int& _x) { _x = _x*_x; });

cout << endl;

for_each(srcVec.begin(), srcVec.end(), [](int _x) { cout << _x << " "; });

cout << endl;

}

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

Усложним задачу, предположим, что мы хотим подсчитать количество элементов контейнера, который обрабатывается функтором, функцией и лямбда-выражением. Для этого им необходимо передать параметр, в который они должны записать искомое значение. В этом случае нам прейдётся переписать код в виде.

#include "stdafx.h"

#include "stdafx.h"

#include <algorithm>

#include <iostream>

#include <vector>

 

using namespace std;

 

class MyFunctor

{

public:

MyFunctor(int& obg_count) :obg_count(obg_count) { }

void operator ()(int& _x) { obg_count++; _x = _x*_x; }

private:

int& obg_count;

};

int f_count = 0;

void print(int _x) { f_count++; cout << _x << " "; }

 

void main()

{

vector<int> srcVec;

for (int val = 0; val < 10; val++)

srcVec.push_back(val);

int obg_count = 0;

for_each(srcVec.begin(), srcVec.end(), MyFunctor(obg_count)); //*

cout << "obg_count = " << obg_count << endl;

for_each(srcVec.begin(), srcVec.end(), print);

cout << "\nf_count = " << f_count << endl;

int l_count = 0;

for_each(srcVec.begin(),srcVec.end(),[&l_count](int _x){l_count++;cout<<_x<<" "; });//**

cout << "\nl_count = " << l_count << endl;

}

На примере этого кода более чётко становятся видны достоинства использования функторов и лямбда-выражений по сравнению с указателями на функции в STL. В функторе (стр. //*) и лямбда-выражении (стр. //**) мы с помощью параметров obg_count и l_count тонко настраиваем эти объекты перед их работой. И в этих же строках мы передаём в алгоритм for_each информацию о функциях ((int& _x){} и (int _x){}), которые он будет вызывать для выполнения действий над элементами контейнера. Указатели на функцию такой гибкостью не обладают. Кроме того, из примера видно насколько короче становится код при использовании лямбда-выражения по сравнению с функтором.

 

 

В примерах приведённых выше, функторы и лямбда-выражения не возвращали значения (т.е. тип возвращаемого значения был void), но в STL есть большое количество алгоритмов, которые требуют, чтобы в качестве параметра им был передан предикат (условие), т.е. функция, которая бы возвращала значение 0 или 1 в зависимости от значения элемента контейнера.

Например, алгоритм count_if() подсчитывает количество элементов контейнера, которые удовлетворяют условию (предикату). У алгоритма count_if() три параметра: итераторы, настроенные на первый и последний элементы над которыми будут выполняться действие, и указатель на функцию, функтор или лямбда-выражение, которые играют роль предиката. Рассмотрим код приведёный ниже:

#include "stdafx.h"

#include <algorithm>

#include <iostream>

#include <vector>

 

using namespace std;

 

void main()

{

vector<int> srcVec;

for (int val = 0; val < 10; val++)

srcVec.push_back(val);

for_each(srcVec.begin(), srcVec.end(), [](int _x) { cout << _x << " "; });

int result =

count_if(srcVec.begin(), srcVec.end(), [](int _x)->bool {return (_x % 2) == 0;}); //*

cout << "result = "<< result << endl;

}

Как мы видем, в строке //* в качестве предиката используем лямбда-выражение. Это лямда-выражение отличается от предыдущих тем, что оно возвращает значение типа bool (->bool) как и положено предикату и соответственно в теле имеет оператор return. После выполнения программы result будет равен 5, так как лябда-выражение (предикат) будет возвращать единицу, только когда элемент контейнера кратен 2. Таких элементов в векторе srcVec ровно 5.

Если в теле лямбда-выражения используется один оператор return, то тип возвращаемого значения (->bool) можно опустить, оно будет вычисленно и подставлено компилятором в код на основании типа выражения, стоящего после оператора return.

Расмотрим пример использования лямбда-выражения с несколькими переменными в списке захвата. Пусть необходимо вычислить количество элеентов вектора srcVec, которые лежат в диапазоне [3,5].

#include "stdafx.h"

#include <algorithm>

#include <iostream>

#include <iterator>

#include <numeric>

#include <vector>

 

using namespace std;

 

void main()

{

vector<int> srcVec;

for (int val = 0; val < 10; val++)

{

srcVec.push_back(val);

}

int lowerBound = 3, upperBound = 5; //1

int result =

count_if(srcVec.begin(), srcVec.end(),

[lowerBound, upperBound](int _n) //mutable //2

{

return lowerBound <= _n && _n <= upperBound; //3

});

cout << result << endl;

}

Необходимо отметить, что переменные lowerBound и upperBound в строках //1 и //2 - это локальные переменные фнкции main, а lowerBound, upperBound в строке //3 - это компонентные данные неименованного функтора, которые по значению были инициализированы их локальными аналогами, стоящими в списке захвата лямбда-выражения (строка //2). Причём, если бы мы захотели изменить значение компонентных данных lowerBound, upperBound в теле лямбда-выражения, то нам бы это не удалось до тех пор, пока мы не расскомментировали бы параметр mutable в стоке //2. Это связано с тем, что если нет mutable, то по умолчанию используется const (т.е. operator()() const{} – константная операция-функция, которая не должна менять значение компонентных данных).

Как мы видим, в лямбда-выражениях сделано всё, чтобы минимизировать количество строк кода. Это касается и списка захвата лямбда-выражения. В таблице ниже приведены различные варианты сокращённой записи списка захвата.

Список-захвата Примечание
[] без захвата переменных из внешней области видимости
[=] все переменные захватываются по значению
[&] все переменные захватываются по ссылке
[x, y] захват x и y по значению
[&x, &y] захват x и y по ссылке
[in, &out] захват in по значению, а out — по ссылке
[=, &out1, &out2] захват всех переменных по значению, кроме out1 и out2, которые захватываются по ссылке
[&, x, &y] захват всех переменных по ссылке, кроме x
[this] захват переменных по значению из области видимости компонентных данных объекта, на который указывает this, при объявлении лямда-выражения в теле компонентной функции.

В заголовке лямбда-выражения можно указать throw-list — список исключений, которые лямбда может сгенерировать. Например, такая лямбда не может генерировать исключения: [] (int _n) throw() { … }, а такая генерирует только bad_alloc: [=] (const std::string & _str) mutable throw(std::bad_alloc) -> bool { … }. Естественно, если его не указывать, то лямбда может генерировать любое исключение.

В финальном варианте стандарта throw-спецификации объявлены устаревшими. Вместо этого оставили ключевое слово noexcept, которое говорит, что функция не должна генерировать исключение вообще.

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

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

Рассмотрим следующий пример:

#include "stdafx.h"

#include <algorithm>

#include <cstdlib>

#include <functional>

#include <iostream>

#include <iterator>

#include <vector>

 

using namespace std;

 

void main()

{

vector<int> myVec;

int init = 0;

generate_n(back_inserter(myVec), 10, [&] { return init++; });

 

function<void(int)> traceLambda = [](int _val) -> void

{

cout << _val << " ";

};

 

for_each(myVec.begin(), myVec.end(), traceLambda);

cout << endl;

 

function<function<int(int)>(int)> lambdaGen =

[](int _val) -> function<int(int)>

{

return [_val](int _n) -> int { return _n + _val; };

};

 

transform(myVec.begin(), myVec.end(), myVec.begin(), lambdaGen(2));

for_each(myVec.begin(), myVec.end(), traceLambda);

cout << endl;

}

Рассмотрим подробнее. Вначале у нас инициализируется вектор с помощью generate_n(). Далее мы создаем переменную traceLambda типа function<void (int)> (то есть функция, принимающая int и возвращающая void) и присваиваем ей лямбда-выражение, которое выводит на консоль значение и пробел. Далее мы используем только что сохраненную лямбду для вывода всех элементов вектора.

После этого мы видим объявление lambdaGen, которая является лямбда-выражением, принимающим один параметр int и возвращающим другую лямбду, принимающую int и возвращающую int.

Следом за этим мы ко всем элементам вектора применяем transform(), в качестве мутационной функции для которого указываем lambdaGen(2). Фактически lambdaGen(2) возвращает другую лямбду, которая прибавляет к переданному параметру число 2 и возвращает результат.

Предыдущий пример можно сделать ещё короче, заменив строки function<void(int)> и function<function<int(int)>(int)> на auto. В этом случае компилятор сам вычислит тип traceLambda и lambdaGen и подстави его в код.

СПИСОК ЛИТЕРАТУРЫ

1. В.В. Подбельский Язык Си++: Учебное пособие. - 5-е изд. - М.: Финансы и статистика, 2000. - 560с. ил.

2. Ален И. Голуб. Си и Си++ правила программирования. - М.: БИНОМ, 1996. - 272с.

3. Бьерн Страуструп Язык программирования Си++ . 3-е изд. /Пер. с англ. - СПб.; М.: «Невский диалект» - «Издательство БИНОМ», 1999. - 991с. ил.

4. Дональд Э. Кнут Искусство программирования. 3-е изд.: Пер. с англ.: Уч.пос. - М.: Издательский дом "Вильяс", 2000. - с., ил.

5. Влах И., Сингхал К. Машинные методы анализа и проектирования электронных схем: Пер. с англ. – М.: Радио и связь, 1988. – 560 с. ил.



Дата добавления: 2020-12-11; просмотров: 387;


Поиск по сайту:

Воспользовавшись поиском можно найти нужную информацию на сайте.

Поделитесь с друзьями:

Считаете данную информацию полезной, тогда расскажите друзьям в соц. сетях.
Poznayka.org - Познайка.Орг - 2016-2024 год. Материал предоставляется для ознакомительных и учебных целей.
Генерация страницы за: 0.029 сек.