STL

std::function и лямбды как параметры: гибкие интерфейсы без шаблонов

Введение

Шаблоны (templates) — мощный инструмент для написания обобщённого кода, но не всегда подходящий: они требуют, чтобы реализация была видна в заголовочном файле, увеливают время компиляции и размер бинарника (instantiation bloat), и не позволяют хранить «функцию» как обычное поле класса с фиксированным типом. std::function решает эту задачу: это полиморфная обёртка, способная хранить любой вызываемый объект (функцию, лямбду, функтор, результат std::bind) за единым типом.

Концепция

std::function<R(Args...)> — это type-erasure обёртка над любым вызываемым объектом с сигнатурой «принимает Args..., возвращает R«. В отличие от шаблонного параметра, тип std::function фиксирован и известен на этапе компиляции независимо от того, что внутри хранится — это позволяет:

  • хранить callback как обычное поле класса (std::function<void()> onClick;);
  • передавать функции в нешаблонные виртуальные методы;
  • менять «поведение» объекта в runtime, просто переписав хранимую функцию.

Цена — небольшой оверхед (динамическая аллокация для крупных захватов лямбды, виртуальный вызов вместо инлайнинга) по сравнению с шаблонным параметром Callable, который инлайнится компилятором.

Пример кода

#include <functional>
#include <iostream>
#include <vector>
#include <string>

// Класс кнопки с гибким обработчиком клика
class Button {
public:
    explicit Button(std::string label) : label_(std::move(label)) {}

    // Поле типа std::function — можно положить лямбду, функцию, функтор
    std::function<void()> onClick;

    void click() {
        std::cout << "Button '" << label_ << "' clickedn";
        if (onClick) { // проверка, что обработчик назначен
            onClick();
        }
    }

private:
    std::string label_;
};

// Функция, принимающая std::function как параметр — пример callback-стиля API
void processItems(const std::vector<int>& items,
                   const std::function<bool(int)>& filter,
                   const std::function<void(int)>& onMatch) {
    for (int item : items) {
        if (filter(item)) {
            onMatch(item);
        }
    }
}

int counterCallback() {
    static int count = 0;
    return ++count;
}

int main() {
    int clickCount = 0;

    Button saveButton("Save");
    // Лямбда с захватом по ссылке
    saveButton.onClick = [&clickCount]() {
        ++clickCount;
        std::cout << "Saving... (click #" << clickCount << ")n";
    };

    Button cancelButton("Cancel");
    // Обычная функция (приводится к std::function автоматически)
    cancelButton.onClick = []() {
        std::cout << "Cancelledn";
    };

    saveButton.click();
    saveButton.click();
    cancelButton.click();

    // std::function как параметр функции: гибкая фильтрация без шаблонов
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int sumOfEven = 0;

    processItems(numbers,
        [](int n) { return n % 2 == 0; },           // filter
        [&sumOfEven](int n) { sumOfEven += n; });    // onMatch

    std::cout << "Sum of even numbers: " << sumOfEven << "n";

    // std::function можно переназначать в runtime
    std::function<int()> counter = counterCallback;
    std::cout << counter() << " " << counter() << " " << counter() << "n";

    return 0;
}

Пояснения к коду

Поле onClick в классе Button имеет тип std::function<void()> — оно может хранить совершенно разные сущности: лямбду с захватом переменных по ссылке (saveButton) или обычную свободную функцию без захвата (cancelButton), и при этом тип поля класса остаётся одинаковым. Это невозможно реализовать через шаблонный параметр класса без превращения самого Button в шаблон.

processItems принимает два std::functionfilter и onMatch — как параметры по константной ссылке (чтобы избежать лишнего копирования обёртки). Это классический паттерн «функция высшего порядка» в нешаблонном виде: сигнатура processItems фиксирована и может быть объявлена в заголовочном файле с реализацией в .cpp, чего нельзя сделать, если параметр — шаблонный Callable&&.

Проверка if (onClick) перед вызовом — стандартный способ убедиться, что std::function не пуста (пустая по умолчанию std::function хранит «ничего» и вызов привёл бы к исключению std::bad_function_call).

Подводные камни и советы

1. std::function — это не бесплатная абстракция: для лямбд с большим захватом возможна динамическая аллокация (если не сработала small buffer optimization конкретной реализации), плюс виртуальный вызов вместо инлайнинга. В горячих циклах с миллионами вызовов это может быть заметно медленнее шаблонного Callable. 2. Если функция шаблонная и реализация доступна в заголовке — предпочитайте template <typename F> void foo(F&& f) вместо std::function ради инлайнинга и нулевого оверхеда. std::function нужен именно там, где требуется единый нешаблонный тип: поля классов, виртуальные методы, ABI-границы, хранение в контейнерах разнородных callback’ов. 3. Пустой std::function вызывает исключение при вызове — всегда проверяйте через if (f) перед вызовом, если не уверены, что обработчик назначен. 4. Захват this в лямбде, сохранённой в std::function, может привести к use-after-free, если объект уничтожается раньше, чем вызывается callback — следите за временем жизни. 5. Для простых случаев «функция или ничего» рассмотрите альтернативу — указатель на функцию (void(*)()) без захвата, если лямбда не захватывает переменные: это легче и без накладных расходов std::function.

================================================================================