Что такое функторы в C++ и для чего они нужны?
Что такое функтор?
Функтор (function object) — это объект класса, в котором перегружен оператор круглые скобки operator(). Благодаря этому такой объект можно вызывать как обычную функцию. В отличие от обычных функций, функторы могут хранить состояние (данные-члены) и иметь несколько перегруженных версий operator().
Зачем нужны функторы, если есть обычные функции?
Главное преимущество функторов — возможность хранить состояние. Обычная функция может полагаться только на глобальные переменные или статические локальные переменные, что неудобно и небезопасно. Функтор же — это полноценный объект, который может содержать поля, инициализированные в конструкторе.
Второе преимущество — гибкость и производительность. В отличие от указателей на функции, компилятор может легко встраивать (inline) вызов operator(), так как тип функтора известен на этапе компиляции. С указателями на функции это часто невозможно, что приводит к накладным расходам.
Реальный пример: сортировка с условием
Допустим, нужно отсортировать вектор чисел в порядке убывания, но с условием: все чётные числа должны идти перед нечётными. С помощью функтора это делается элегантно:
#include <iostream>
#include <vector>
#include <algorithm>
class EvenFirst {
public:
// Конструктор принимает пороговое значение
EvenFirst(bool descending = true) : descending_(descending) {}
bool operator()(int a, int b) const {
// Чётные числа считаем "меньшими" (должны быть раньше)
if ((a % 2 == 0) && (b % 2 != 0)) return true;
if ((a % 2 != 0) && (b % 2 == 0)) return false;
// Если оба чётные или оба нечётные — сортируем по убыванию
return descending_ ? a > b : a < b;
}
private:
bool descending_;
};
int main() {
std::vector<int> data = {3, 8, 1, 6, 4, 7, 2, 5};
// Создаём функтор с настройкой: чётные вперёд, внутри групп — по убыванию
EvenFirst comp(true);
std::sort(data.begin(), data.end(), comp);
for (int x : data) {
std::cout << x << " ";
}
// Вывод: 8 6 4 2 7 5 3 1
return 0;
}
Обратите внимание: в функторе хранится поле descending_, которое определяет порядок сортировки внутри групп. Обычной функцией такое сделать сложнее — пришлось бы использовать глобальную переменную или передавать параметр через замыкание (что в C++03 было невозможно).
Использование в STL контейнерах
Функторы часто применяются для задания пользовательских компараторов в ассоциативных контейнерах:
#include <set>
#include <string>
struct CaseInsensitiveCompare {
bool operator()(const std::string& a, const std::string& b) const {
// Сравнение без учёта регистра (упрощённо)
return std::lexicographical_compare(
a.begin(), a.end(), b.begin(), b.end(),
[](char c1, char c2) { return std::tolower(c1) < std::tolower(c2); }
);
}
};
int main() {
std::set<std::string, CaseInsensitiveCompare> words;
words.insert("Apple");
words.insert("apple"); // не добавится, так как "Apple" уже есть
words.insert("Banana");
return 0;
}
Функторы с несколькими состояниями
Функторы могут накапливать данные. Классический пример — подсчёт статистики:
class AverageCalculator {
public:
AverageCalculator() : sum_(0), count_(0) {}
void operator()(double value) {
sum_ += value;
++count_;
}
double getAverage() const {
return count_ == 0 ? 0.0 : sum_ / count_;
}
private:
double sum_;
int count_;
};
int main() {
std::vector<double> values = {1.5, 2.3, 4.7, 3.1};
AverageCalculator calc;
calc = std::for_each(values.begin(), values.end(), calc);
std::cout << "Average: " << calc.getAverage(); // 2.9
return 0;
}
Вывод
- Функтор — это объект с перегруженным
operator(). - Преимущества: хранение состояния, возможность встраивания компилятором, гибкость настройки.
- Применение: алгоритмы STL, компараторы для контейнеров, обратные вызовы, ленивые вычисления.
- В современном C++ (C++11 и новее) функторы часто заменяются лямбда-выражениями, но понимание функторов необходимо для работы с
std::bind,std::functionи для написания обобщённого кода.
Дополнение: функторы vs лямбды
В современном C++ (начиная с C++11) лямбда-выражения во многих случаях заменили функторы. Лямбда — это по сути синтаксический сахар для создания анонимного функтора. Например, предыдущий пример с сортировкой можно переписать так:
std::sort(data.begin(), data.end(),
[descending = true](int a, int b) {
if ((a % 2 == 0) && (b % 2 != 0)) return true;
if ((a % 2 != 0) && (b % 2 == 0)) return false;
return descending ? a > b : a < b;
});
Однако функторы остаются полезными, когда:
- Нужно создать именованный тип с явной семантикой (документирование).
- Требуется множество перегруженных версий
operator(). - Функтор должен быть передан в шаблонный параметр (например, как компаратор для
std::set). Лямбды — это анонимные типы, и их нельзя использовать напрямую в качестве параметров шаблона (хотя в C++20 это стало возможным). - Нужна поддержка до C++11 (старые компиляторы).
Также функторы позволяют использовать наследование и виртуальные функции, что невозможно для лямбд. Например, можно создать иерархию функторов для разных стратегий сравнения.
В целом, для простых случаев используйте лямбды, для сложной логики с состоянием и полиморфизмом — функторы.