std::function и std::bind
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 86 из 104.
Хотя лямбда-выражения мощны, иногда возникает необходимость хранить вызываемые объекты разных типов единообразным способом или адаптировать существующие функции под требуемую сигнатуру. std::function и std::bind из заголовочного файла <functional> решают эти задачи.
std::function — это обертка с удалением типов (type-erased wrapper), которая может хранить любой вызываемый объект, соответствующий определенной сигнатуре: функции, лямбда-выражения или функциональные объекты:
#include <iostream>
#include <functional>
int add(int a, int b) { return a + b; }
int main() {
std::function<int(int, int)> operation;
operation = add; // Обычная функция
std::cout << operation(3, 4) << "\n"; // 7
operation = [](int a, int b) { return a * b; }; // Лямбда-выражение
std::cout << operation(3, 4) << "\n"; // 12
}std::bind создает новый вызываемый объект, фиксируя некоторые аргументы существующей функции. Используйте std::placeholders::_1, _2 и т. д., чтобы обозначить аргументы, которые остаются переменными:
#include <iostream>
#include <functional>
void greet(const std::string& greeting, const std::string& name) {
std::cout << greeting << ", " << name << "!\n";
}
int main() {
using namespace std::placeholders;
auto sayHello = std::bind(greet, "Hello", _1);
sayHello("Alice"); // Hello, Alice!
auto swapped = std::bind(greet, _2, _1);
swapped("Bob", "Hi"); // Hi, Bob!
}Эти инструменты особенно полезны в ООП для хранения обратных вызовов в качестве членов класса или реализации паттерна «Стратегия». Однако современный C++ часто отдает предпочтение лямбда-выражениям вместо std::bind для лучшей читаемости и производительности — используйте std::bind в основном тогда, когда вам нужно изменить порядок аргументов или работать с устаревшим кодом.
Задание
ЛегкоДавайте создадим настраиваемый калькулятор, который демонстрирует возможности std::function и std::bind. Вы создадите систему, в которой математические операции можно сохранять, заменять и настраивать во время выполнения программы, показывая, как эти инструменты обеспечивают гибкое управление обратными вызовами (callbacks).
Вы организуете свой код в трех файлах:
MathOperations.h: Определите коллекцию автономных математических функций, которые будут служить вашей библиотекой операций.Создайте следующие функции:
add(int a, int b)— возвращает суммуsubtract(int a, int b)— возвращает разность (a - b)multiply(int a, int b)— возвращает произведениеpower(int base, int exponent, int multiplier)— возвращаетmultiplier * (base ^ exponent). Используйте простой цикл для вычисления степени (предполагается, что показатели степени неотрицательны).
Calculator.h: Определите классCalculator, который используетstd::functionдля динамического хранения и выполнения операций.Ваш Calculator должен иметь:
- Приватный член
std::function<int(int, int)>для хранения текущей бинарной операции setOperation(std::function<int(int, int)> op)— устанавливает текущую операциюcalculate(int a, int b)— выполняет сохраненную операцию и возвращает результат
Включите необходимые заголовки (
<functional>) и обязательно подключите заголовочный файл MathOperations.- Приватный член
main.cpp: Считайте три входных значения:- Название операции:
add,subtract,multiplyилиsquare - Первое число (целое)
- Второе число (целое)
Создайте объект
Calculatorи настройте его в зависимости от названия операции:- Для
add,subtractиmultiply: назначьте соответствующую функцию напрямую калькулятору - Для
square: используйтеstd::bind, чтобы создать специализированную версиюpower, которая всегда использует показатель степени 2 и множитель 1. Привязанная функция должна принимать два аргумента, где только первый используется в качестве основания (второй аргумент можно игнорировать с помощью плейсхолдера).
После установки операции вызовите
calculate()с вашими двумя числами и выведите:Result: [value]Затем продемонстрируйте замену операций, назначив калькулятору лямбда-выражение, которое возвращает
a + b + 100, запустите его с теми же входными данными и выведите:With bonus: [value]- Название операции:
Например, при входных данных add, 10 и 5:
Result: 15
With bonus: 115При входных данных multiply, 7 и 3:
Result: 21
With bonus: 110При входных данных square, 4 и 0:
Result: 16
With bonus: 104Эта задача показывает, как std::function обеспечивает единый способ хранения различных вызываемых типов (обычных функций, привязанных функций и лямбда-выражений), в то время как std::bind позволяет адаптировать существующие функции, фиксируя некоторые из их аргументов.
Шпаргалка
Заголовочный файл <functional> предоставляет std::function и std::bind для гибкой работы с вызываемыми объектами.
std::function — это обертка со стиранием типов, которая может хранить любой вызываемый объект, соответствующий определенной сигнатуре:
#include <functional>
std::function<int(int, int)> operation;
operation = add; // Обычная функция
operation = [](int a, int b) { return a * b; }; // Лямбдаstd::bind создает новый вызываемый объект, фиксируя некоторые аргументы существующей функции. Используйте std::placeholders::_1, _2 и т. д. для переменных аргументов:
#include <functional>
void greet(const std::string& greeting, const std::string& name) {
std::cout << greeting << ", " << name << "!\n";
}
using namespace std::placeholders;
auto sayHello = std::bind(greet, "Hello", _1); // Фиксация первого аргумента
sayHello("Alice"); // Hello, Alice!
auto swapped = std::bind(greet, _2, _1); // Изменение порядка аргументов
swapped("Bob", "Hi"); // Hi, Bob!Эти инструменты полезны для хранения обратных вызовов в качестве членов класса или реализации гибких систем обратных вызовов. В современном C++ для лучшей читаемости часто предпочитают лямбда-выражения вместо std::bind.
Попробуйте сами
#include <iostream>
#include <string>
#include <functional>
#include "Calculator.h"
using namespace std;
int main() {
// Чтение входных данных
string operation;
int num1, num2;
cin >> operation >> num1 >> num2;
// Создание экземпляра Calculator
Calculator calc;
// TODO: Настройте калькулятор в зависимости от названия операции
// Для "add", "subtract", "multiply": назначьте соответствующую функцию напрямую
// Для "square": используйте std::bind для создания специализированной версии power
// которая всегда использует экспоненту 2 и множитель 1
// Hint: Используйте std::placeholders::_1 для базового аргумента
if (operation == "add") {
// Ваш код здесь
} else if (operation == "subtract") {
// Ваш код здесь
} else if (operation == "multiply") {
// Ваш код здесь
} else if (operation == "square") {
// Ваш код здесь — используйте std::bind с функцией power
}
// TODO: Вызовите calculate() и выведите результат
// Формат: "Result: [value]"
// TODO: Продемонстрируйте смену операций
// Назначьте калькулятору лямбда-выражение, которое возвращает a + b + 100
// Снова вызовите calculate() и выведите результат
// Формат: "With bonus: [value]"
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер12Возможности современного C++
Семантика перемещения и RvaluesИдеальная передача (Perfect Forwarding)Лямбда-выражения в деталяхstd::function и std::bindconstexpr и constevalСтруктурированные привязкиoptional, variant, any