Функторы и лямбда-выражения.
Функторами называют классы, в которых переопределена операция-функция 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; просмотров: 465;