§18 Функторы. Предикаты. Лямбда-выражения

Функциональные объекты

Функторы

Функциональным объектом (function object) или функтором (functor) называют структуру (или класс) в котором используется перегрузка операции вызова функции – (). Использование функторов и лямбда-выражений (обсуждаются ниже) вместо обычных функций позволяет повысить производительность программы и получить большую гибкость, например, функтор можно передавать как шаблонный параметр, функтор хранит значения полей, которые можно изменять или получать к ним доступ в продолжении работы программы. Устройство функтора имеет следующий синтаксис:

struct Имя_функтора {
    void operator() () {
    Инструкции;
    }
};

Важно понимать, что инструкция Имя_функтора() означает не вызов функции, а вызов перегруженной операции () для функционального объекта. В следующей задаче функциональный объект (mult()) применяется к каждому элементу массива с помощью алгоритма for_each(). Постановка задачи. Дан массив. Увеличить каждый элемент в два раза.
Программа 18.1

#include <iostream>
#include <algorithm>
#include <array>
using namespace std;

struct mult {
	void operator() (int &n) {
		n *= 2;
	}
};

int main() {
	mult k;
	array<int, 5> num {3, 4, 5, 6, 7};
	for_each(num.begin(), num.end(), mult());
	for (auto &r : num)
		cout << r << " ";
	return 0;
}

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

#include <iostream>
#include <algorithm>
#include <array>
using namespace std;

struct Sum {
	int s, i;
	// Инициализацию начальных значений
	// производим в конструкторе
	Sum() : s(0), i(0) {}
	void operator() (int &n) {
		i++;
		s += n;
	}
};

int main() {
	Sum ar;
	array<int, 10> num {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	ar = for_each(num.begin(), num.end(), Sum());
	cout << ar.s << "\n"
		 << ar.i << endl;
	return 0;
}

В этой программе объекту ar присваивается возвращаемый алгоритмом for_each функтор Sum, в результате мы получаем доступ к значению полей объекта.
Примечание. Существуют встроенные функции (accumulate и partial_sum, заголовок <numeric>) реализующие суммирование диапазонов. Таким образом, функтор, для суммирования, имеет смысл создавать только в том случае, если помимо суммирования выполняются еще какие-либо операции.

Предикаты

Функциональные объекты, которые возвращают булевский тип, называются предикатами. Предикаты используются в тех алгоритмах, которые имеют “логические” суффиксы _if, _if_not (например, count_if, find_if, copy_if и т. д.). Рассмотрим пример использования предиката на примере алгоритма count_if.
Для того, чтобы передавать предикату критерий, необходимо в теле структуры создать конструктор, например так, как показано в программе 18.3.
Дан массив. Определить количество элементов которые > 3.
Программа 18.3

#include <iostream>
#include <algorithm>
#include <array>
using namespace std;

struct Prd {
	int my_cnt;
	// Конструктор
	Prd(const int &t) : my_cnt(t) {}
	// Перегрузка операции ()
	bool operator() (const int & v) {
		return v > my_cnt;
	}
};

int main() {
	array<int, 10> num {1, 2, 3, 4, 5, 3, 7, 3, 9, 3};
	cout << count_if(num.begin(), num.end(), Prd(3)) << endl;
	return 0;
}

Понять суть происходящего можно представив, что вызывается функция, как аргумент другой функции, то есть:

operator()(arg_prd, Prd(arg_crt))

где arg_prd передаваемый предикату элемент массива, а arg_crt – аргумент-критерий метода Prd(). Но, поскольку мы имеем дело с классом, то мы говорим, что вторым аргументом вызывается наш конструктор. Вот в этом и раскрывается преимущество функторов, по сравнению с обычными функциями.
В следующей задаче предикату передаётся уже два аргумента. Постановка задачи. Дан массив. Определить количество элементов, которые кратны 3 и больше либо равны (!<) 5. (В нашей программе аргументами функтора могут выступать произвольные числа). Покажем на этом примере, что функторы (как функции и классы) можно использовать как шаблоны.
Программа 18.4

#include <iostream>
#include <algorithm>
#include <array>
using namespace std;

template<typename T>
struct Prd {
    T a, b;
    Prd(const T &t1, const T &t2) : a(t1), b(t2) {}
    bool operator() (T &n) {
        return !(n % a) && !(n < b);
    }
};

int main() {
    array<int, 10> num {1, 2, 3, 9, 11, 6, 7, 8, 9, 10};
    cout << count_if(num.begin(), num.end(), Prd<int>(3, 5)) << endl;
    return 0;
}

Когда функтор является шаблоном, то необходимо вместе с именем функтора передавать и тип аргумента(-ов) Prd<int>.

Предопределенные функторы. Функциональные адаптеры

Если включить заголовочный файл <functional>, то появится возможность использования набора предопределенных вспомогательных функторов и предикатов, созданных для удобной работы с алгоритмами. Для всех предопределенных функторов необходимо указывать тип аргумента, например: greater<int>.
Предопределенные функциональные объекты

Название	Кол-во операндов	Возвращаемый тип	Действие
Сравнения
equal_to	Бинарный		bool			x == y
not_equal_to	Бинарный		bool			x != y
greater		Бинарный		bool			x > y
less		Бинарный		bool			x < y
greater_equal	Бинарный		bool			x >= y
less_equal	Бинарный		bool			x <= y
Логические	
logical_and	Бинарный		bool			x && y
logical_or	Бинарный		bool			x || y
logical_not	Унарный			bool			!x
Арифметические
plus		Бинарный		T			x + y
minus		Бинарный		T			x - y
multiplies	Бинарный		T			x * y
divides		Бинарный		T			x / y
modulus		Бинарный		T			x % y
negate		Унарный			T			-x
Битовые
bit_and		Бинарный		T			x & y
bit_or		Бинарный		T			x | y
bit_xor		Бинарный		T			x ^ y
bit_not		Унарный			T			~x

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

  • bind() – связывает аргументы с операцией
  • mem_fn() - вызывает операцию указывающую на функцию-член объекта
  • not1() – унарное отрицание
  • not2() – бинарное отрицание

С помощью bind() можно связать аргументы вызываемых объектов как непосредственно (указав, например, конкретное значение) или с помощью заполнителей. Заполнители (_1, _2, _3, ...) определены в пространстве имен placeholders. Чтобы не упоминать это пространство имен в программе необходимо воспользоваться директивой:

using namespace placeholders;

Рассмотрим часто используемые функторы и предикаты на примере решения следующей задачи. Дан массив из 20 элементов. Произвести следующие операции, используя алгоритмы и предопределенные функторы:

  1. Выполнить сортировку по убыванию
  2. Подсчитать количество элементов по критерию
  3. Заменить элементы по критерию

Программа 18.5

#include <iostream>
#include <algorithm>
#include <functional>
#include <random>
#include <ctime>
#include <array>
using namespace std;
using namespace placeholders;

struct Rnd {
	void operator() (int &ar) {
		static default_random_engine rnd(time(0));
		static uniform_int_distribution<unsigned> d(10, 30);
		ar = d(rnd);
	}
};

int main() {
	array<int, 20> num {};
	auto first = num.begin();
	auto last = num.end();
	for_each(first, last, Rnd());
	cout << "Исходный массив:\n";
	for (auto &ar : num)
		cout << ar << " ";
	//=====================================================
	// Сортировка по убыванию
	//=====================================================
	cout << "\nУпорядоченный массив по убыванию:\n";
	sort(first, last, greater<int>());
	for (auto &ar : num)
		cout << ar << " ";
	//=====================================================
	// Подсчет по критерию
	//=====================================================
	cout << "\nКоличество найденных элементов равных 20:\n"
		 << count_if(first, last, bind(equal_to<int>(), _1, 20));
	//=====================================================
	// Замена по критерию
	//=====================================================
	replace_if(first, last, bind(less<int>(), _1, 19), 10);
	cout << "\nЗаменены все < 19 на 10:\n";
	for (auto &ar : num)
		cout << ar << " ";
	cout << endl;
	return 0;
}

Возможный вывод:

Исходный массив:
15 21 25 21 28 18 10 21 11 20 23 19 14 30 20 22 15 29 25 14 
Упорядоченный массив по убыванию:
30 29 28 25 25 23 22 21 21 21 20 20 19 18 15 15 14 14 11 10 
Количество найденных элементов равных 20:
2
Заменены все < 19 на 10:
30 29 28 25 25 23 22 21 21 21 20 20 19 10 10 10 10 10 10 10 

С помощью адаптеров можно конструировать довольно сложные связки, использующие вложенные адаптеры. Но это может существенно усложнить код и сделать его "не читаемым". Всё-таки синтаксис функциональных объектов в С++ довольно сложен. Есть гораздо более простые решения в виде лямбда-выражений.

Лямбда-выражения

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

1. [список захвата] (список параметров) mutable -> возвращаемый тип {тело функции}
2. [список захвата] (список параметров) -> возвращаемый тип {тело функции}
3. [список захвата] (список параметров) {тело функции}
4. [список захвата] {тело функции}
  1. Полная форма. Позволяет изменять параметры, захваченные копированием.
  2. Определение константной лямбды: объекты, захваченные копированием, не могут изменяться.
  3. Без указания типа. Возвращаемый тип будет типом возвращаемого выражения или типом void.
  4. Пропущен список параметров: функция не принимает аргументов.

Список захвата определяет, какие объекты, видимые в области определения функции, ​​будут видны внутри тела функции. Список передается следующим образом:

  • [a,&b] a захвачена по значению, а b захвачена по ссылке.
  • [this] захватывает указатель this по значению.
  • [&]    захват всех объектов по ссылке
  • [=]    захват всех объектов по значению
  • []     ничего не захватывать

Примеры использования лямбда-выражений в следующей программе:
Программа 18.6

#include <iostream>
#include <algorithm>
#include <random>
#include <ctime>
#include <array>
using namespace std;

int main() {
	array<int, 20> num {};
	// Указание static необходимо, иначе лямбда выдаст ошибку
	static default_random_engine rnd(time(0));
	static uniform_int_distribution<unsigned> d(10, 99);
	auto first = num.begin();
	auto last = num.end();
	for_each(first, last, [](int &el){el = d(rnd);});
	cout << "Исходный массив:\n";
	for (auto &ar : num)
		cout << ar << " ";
	int k = 0;
	//=============================================
	// Cумма элементов
	//=============================================
	for_each(first, last, [&k](int el){k += el;});
	cout << "\nСумма элементов: " << k << endl;
	//=============================================
	// Уменьшить в 10 раз каждый элемент
	//=============================================
	for_each(first, last, [](int &el){el /= 10;});
	for (auto &ar : num)
		cout << ar << " ";
	//=============================================
	// Считать только те, которые > 5
	//=============================================
	k = count_if(first, last, [](int &el){return el > 5;});
	cout << "\nКоличество элементов > 5: " << k << endl;
	//=============================================
	// Заменить те, которые < 5 на 9
	//=============================================
	replace_if(first, last, [](int &el){return el < 5;}, 9);
	cout << "Измененный массив: " << endl;
	for (auto &ar : num)
		cout << ar << " ";
	return 0;
}

Возможный вывод:

Исходный массив:
35 97 80 41 95 98 40 44 16 50 78 65 10 73 26 58 34 80 85 82 
Сумма элементов: 1187
3 9 8 4 9 9 4 4 1 5 7 6 1 7 2 5 3 8 8 8 
Количество элементов > 5: 10
Измененный массив: 
9 9 8 9 9 9 9 9 9 5 7 6 9 7 9 5 9 8 8 8
Литература
  1. Прата, Стивен. Язык программирования C++. Лекции и упражнения, 6-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильяме», 2012
  2. Липпман Б. Стенли, Жози Лажойе, Барбара Э. Му. Язык программирования С++. Базовый курс. Изд. 5-е. М: ООО "И. Д. Вильямс", 2014
  3. Эллайн А. C++. От ламера до программера. СПб.: Питер, 2015
  4. Джосаттис Н. М. Стандартная библиотека C++: справочное руководство, 2-е изд.-М.: Вильямс, 2014


Print Friendly, PDF & Email

Comments are closed.