Функторы в C++: что это и как упрощают код? Полный гайд

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое функторы в 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(). Позволяют создавать сложные цепочки вызовов. Легко тестируются и пере

Блоги

Рекомендаций по книгам