Что такое функторы в C++ и зачем они нужны?
Представьте, что вы пишете код на C++ и вам нужно передать поведение (функцию) в другую функцию, например, для сортировки или фильтрации данных. Классическое решение — указатели на функции. Но у них есть недостатки: они не хранят состояние, их сложно встраивать (inline), и они выглядят немного архаично. Тут на сцену выходят функторы в C++ (они же объекты-функции или функциональные объекты C++).
Функтор — это обычный класс или структура, в которой переопределен оператор вызова функции operator(). Благодаря этому, экземпляр такого класса можно использовать как обычную функцию. Звучит сложно? На самом деле это один из самых элегантных инструментов C++. На Stack Overflow вопрос "What are C++ functors and their uses?" набрал тысячи просмотров — разработчики хотят разобраться в этой теме, чтобы писать более гибкий и производительный код. В этом руководстве мы разложим всё по полочкам: от базового синтаксиса до реальных примеров использования с STL.
Базовый синтаксис: как создать свой первый функтор
Создание функтора сводится к трем шагам:
- Объявить класс или структуру.
- Реализовать внутри
operator()с нужной сигнатурой (параметры и возвращаемый тип). - Создать объект этого класса и вызвать его как функцию.
Вот простейший пример функтора C++, который умножает число на два:
#include <iostream>
class Multiplier {
public:
// Переопределяем оператор ()
int operator()(int x) const {
return x * 2;
}
};
int main() {
Multiplier mult; // Создаем объект-функцию
std::cout << mult(5) << std::endl; // Вывод: 10
std::cout << mult(10) << std::endl; // Вывод: 20
return 0;
}
Обратите внимание: mult(5) выглядит как вызов обычной функции, но на самом деле это вызов mult.operator()(5). Компилятор делает синтаксический сахар, и код становится чистым и интуитивным.
Главное преимущество: функторы могут хранить состояние
В отличие от обычных функций или лямбда-выражений (до C++11), объекты функции C++ могут иметь собственные поля-члены. Это открывает огромные возможности. Например, вы можете создать функтор, который не просто выполняет действие, но и запоминает, сколько раз его вызвали:
#include <iostream>
class Counter {
private:
int count = 0;
public:
int operator()() {
return ++count;
}
int getCount() const { return count; }
};
int main() {
Counter counter;
std::cout << counter() << std::endl; // 1
std::cout << counter() << std::endl; // 2
std::cout << counter() << std::endl; // 3
// Состояние сохраняется между вызовами!
return 0;
}
Этот пример функтора C++ с состоянием невозможно реализовать с помощью простой функции без использования глобальных переменных (что плохо). Функторы решают эту проблему элегантно и безопасно.
Функторы и алгоритмы STL: идеальное сочетание
Самое мощное применение функторов — это работа с алгоритмами из Standard Template Library (STL). Алгоритмы вроде std::sort, std::find_if, std::for_each ожидают на вход вызываемый объект (callable). Вы можете передать им функтор, и он будет выполнять нужную логику.
Пример: сортировка с функтором
Допустим, у вас есть вектор чисел, и вы хотите отсортировать их по убыванию. Можно написать собственный функтор сравнения:
#include <iostream>
#include <vector>
#include <algorithm>
class Descending {
public:
bool operator()(int a, int b) const {
return a > b; // Возвращаем true, если a должно идти перед b
}
};
int main() {
std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6};
std::sort(data.begin(), data.end(), Descending());
for (int n : data) {
std::cout << n << " ";
}
// Вывод: 9 6 5 4 3 2 1 1
return 0;
}
Обратите внимание: мы передали Descending() — временный объект функтора. Алгоритм std::sort будет вызывать его каждый раз, когда нужно сравнить два элемента. Код получается читаемым и модульным.
Пример: фильтрация с настраиваемым порогом
Ещё один классический пример использования функторов в C++ — это проверка элементов на соответствие условию. Создадим функтор, который проверяет, больше ли число заданного порога:
#include <iostream>
#include <vector>
#include <algorithm>
class GreaterThan {
private:
int threshold;
public:
GreaterThan(int t) : threshold(t) {}
bool operator()(int value) const {
return value > threshold;
}
};
int main() {
std::vector<int> data = {10, 25, 3, 40, 55, 7};
// Находим первое число, больше 20
auto it = std::find_if(data.begin(), data.end(), GreaterThan(20));
if (it != data.end()) {
std::cout << "Первое число больше 20: " << *it << std::endl; // 25
}
return 0;
}
Здесь функтор GreaterThan хранит пороговое значение как состояние. Если бы мы использовали обычную функцию, нам пришлось бы передавать порог через глобальную переменную или связывать аргументы с помощью std::bind. Функтор делает код чище.
Функторы vs указатели на функции vs лямбды
Многие новички спрашивают: "Зачем нужны функторы, если есть указатели на функции или лямбды?". Давайте сравним:
- Указатели на функции: Не могут хранить состояние. Медленнее из-за косвенного вызова (компилятор редко может встроить их в код). Синтаксис громоздкий.
- Лямбда-выражения (C++11 и новее): По сути, это синтаксический сахар над функторами. Компилятор создает анонимный функтор. Лямбды удобны для одноразового использования, но если логика повторяется в нескольких местах, лучше вынести её в именованный функтор.
- Функторы: Дают полный контроль. Могут иметь множество методов, не только
operator(). Позволяют создавать сложные цепочки вызовов. Легко тестируются и пере