Введение
Шаблоны (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::function — filter и 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.
================================================================================