Avatar ·

Что такое функторы в C++ и для чего они нужны?

Привет, коллеги! Я начал изучать C++ и наткнулся на термин "функтор". Читал документацию, но до конца не понял. Вроде бы это объект класса, который ведёт себя как функция. Но зачем такое нужно? У нас же есть обычные функции, лямбды, указатели на функции. Я попробовал написать простой пример, но не уверен, что это правильное использование. Вот что у меня получилось: ```cpp #include class Adder { public: int operator()(int a, int b) { return a + b; } }; int main() { Adder add; std::cout << add(3, 4) << std::endl; // Выводит 7 return 0; } ``` Я понимаю, что это работает, но чем это лучше обычной функции `int add(int a, int b)`? Зачем городить целый класс? Я слышал, что функторы используются в STL, например, в алгоритмах. Но мне не ясно, как именно и в каких ситуациях они дают преимущество. Можете объяснить "на пальцах": что такое функторы, какие у них есть плюсы по сравнению с обычными функциями и указателями на функции, и приведите реальные примеры использования (желательно с кодом)? Особенно интересует, как они работают с контейнерами и алгоритмами STL. Заранее спасибо! <<>>

Что такое функтор?

Функтор (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 (старые компиляторы).

Также функторы позволяют использовать наследование и виртуальные функции, что невозможно для лямбд. Например, можно создать иерархию функторов для разных стратегий сравнения.

В целом, для простых случаев используйте лямбды, для сложной логики с состоянием и полиморфизмом — функторы.