深入理解C++ 中 `<functional>` 头文件


📋 文章概述

<functional> 头文件是 C++ 标准库中最强大、最灵活的部分之一。它提供了处理可调用对象(callable objects)的工具——任何可以像函数一样调用的东西。无论你使用的是 lambda 表达式、函数指针、函数对象(functor)还是成员函数,<functional> 都能帮助你无缝地存储、绑定、适配和传递它们。

在本文中,我们将通过清晰实用的示例,探索 <functional> 中的每一个重要组件。读完本文,你将掌握如何使用 std::functionstd::bind、占位符、std::reference_wrapperstd::mem_fn 等工具。


1️⃣ <functional> 是什么?

核心来说,<functional> 提供以下功能:

组件 作用
多态函数包装器 std::function 类型擦除的容器,可存储任何可调用对象
函数绑定器 std::bind + 占位符 固定参数或重新排列参数顺序
引用包装器 std::reference_wrapper / std::ref / std::cref 在需要拷贝的上下文中存储引用
成员函数适配器 std::mem_fn 将成员函数转换为普通函数对象
内置函数对象 算术运算、比较、逻辑操作(如 std::plusstd::greaterstd::logical_and

所有这些组件都能与 <algorithm> 库和现代 C++ 编程范式无缝协作。


2️⃣ std::function — 通用可调用对象包装器

std::function 是一个类型擦除包装器,可以存储任何具有指定签名的可调用对象。它就像一个"超级函数指针"。

🔹 基本用法

#include <iostream>
#include <functional>

int add(int a, int b) { return a + b; }

struct Multiply {
    int operator()(int a, int b) const { return a * b; }
};

int main() {
    // 存储普通函数
    std::function<int(int,int)> func = add;
    std::cout << func(3, 4) << '\n';  // 输出: 7

    // 存储 lambda 表达式
    func = [](int a, int b) { return a - b; };
    std::cout << func(10, 3) << '\n'; // 输出: 7

    // 存储函数对象(functor)
    Multiply mult;
    func = mult;
    std::cout << func(5, 6) << '\n';   // 输出: 30

    return 0;
}

🔹 为什么使用 std::function

🔹 性能提示

std::function 有一定开销(虚函数调度、可能的小对象优化)。在性能敏感路径中,考虑使用模板或原始函数指针。但对于回调、事件系统或配置场景,它非常宝贵。

🔹 示例:回调函数

#include <functional>
#include <vector>

class Button {
    std::vector<std::function<void()>> clickHandlers;
public:
    void onClick(std::function<void()> handler) {
        clickHandlers.push_back(handler);
    }
    void click() {
        for (auto& h : clickHandlers) h();
    }
};

void playSound() { /* ... */ }

int main() {
    Button btn;
    btn.onClick(playSound);
    btn.onClick([]{ std::cout << "Button clicked!\n"; });
    btn.click(); // 调用两个处理器
}

3️⃣ std::bind — 固定和重排参数

在 lambda 表达式出现之前(C++11),std::bind 是创建适配器的主要方式。如今 lambda 通常更清晰,但 std::bind 在泛型代码或需要绑定成员函数时仍有优势。

🔹 语法

auto newCallable = std::bind(callable, arg1, arg2, ..., argN);

占位符 _1_2…(来自 std::placeholders)表示调用绑定对象时将传入的参数。

🔹 基础示例

#include <iostream>
#include <functional>

void print(int a, int b, int c) {
    std::cout << a << ", " << b << ", " << c << '\n';
}

int main() {
    using namespace std::placeholders;

    // 固定第一个参数为 10
    auto f1 = std::bind(print, 10, _1, _2);
    f1(20, 30);               // 输出: 10, 20, 30

    // 重排参数:第三个变第一个
    auto f2 = std::bind(print, _3, _1, _2);
    f2(1, 2, 3);              // 输出: 3, 1, 2

    // 绑定成员函数(需要对象指针)
    struct Demo { void say(int x) { std::cout << x << '\n'; } };
    Demo d;
    auto f3 = std::bind(&Demo::say, &d, _1);
    f3(42);                   // 输出: 42
}

🔹 何时优先使用 std::bind 而非 lambda?

lambda 通常更易读。新代码建议优先使用 lambda,除非有特定理由。


4️⃣ 占位符 — _1, _2, _3

占位符位于 std::placeholders 命名空间中,用于告诉 std::bind 转发哪些参数以及放在什么位置。

#include <functional>
#include <iostream>

void show(int a, int b) { std::cout << a << ' ' << b << '\n'; }

int main() {
    using namespace std::placeholders;

    auto f = std::bind(show, _2, _1);  // 交换参数
    f(100, 200); // 输出: 200 100

    // 也可以忽略参数:在未使用的位置使用 _1
    auto g = std::bind(show, 42, _2);  // 忽略第一个传入参数
    g(999, 123); // 输出: 42 123
}

5️⃣ std::refstd::cref — 引用包装器

默认情况下,std::bindstd::function拷贝它们的参数。要存储或传递引用,请使用 std::ref(非常量)或 std::cref(常量)。它们创建 std::reference_wrapper<T>

🔹 为什么需要它们?

#include <functional>
#include <iostream>

void increment(int& x) { ++x; }

int main() {
    int a = 5;
    auto bad = std::bind(increment, a);   // 拷贝了 'a'!
    bad();
    std::cout << a << '\n';               // 仍是 5 - 未增加

    auto good = std::bind(increment, std::ref(a));
    good();
    std::cout << a << '\n';               // 6 - 成功!
}

🔹 在 std::function 中使用 std::ref

std::function<void()> f = std::bind(increment, std::ref(a));

也可以直接存储引用包装器:

std::reference_wrapper<int> refA = std::ref(a);
refA.get() = 10;   // 修改 a

🔹 何时使用 std::cref

当你想传递常量引用时,例如避免将大对象拷贝到 lambda 或绑定的可调用对象中:

void process(const std::string& s);
std::string huge = "...";
auto bound = std::bind(process, std::cref(huge)); // 无拷贝

6️⃣ std::mem_fn — 包装成员函数

std::mem_fn 将成员函数指针转换为可调用对象,调用时需传入对象(或指针)作为第一个参数。

🔹 语法

auto wrapper = std::mem_fn(&Class::method);
wrapper(object, args...);      // 传入对象引用
wrapper(&object, args...);     // 传入对象指针

🔹 示例

#include <functional>
#include <iostream>
#include <vector>
#include <algorithm>

struct Person {
    std::string name;
    void greet() const { std::cout << "Hello, I'm " << name << '\n'; }
    void setAge(int a) { age = a; }
    int age = 0;
};

int main() {
    std::vector<Person> people = {{"Alice"}, {"Bob"}};

    // 在每个对象上调用 const 成员函数
    auto greetFn = std::mem_fn(&Person::greet);
    for (Person& p : people) greetFn(p);   // Alice, 然后 Bob

    // 与 std::for_each 配合使用
    std::for_each(people.begin(), people.end(), greetFn);

    // 非 const 成员函数,使用指针
    auto setAgeFn = std::mem_fn(&Person::setAge);
    Person alice{"Alice"};
    setAgeFn(&alice, 30);   // 传入指针
    setAgeFn(alice, 31);    // 传入引用也可
}

🔹 与 std::bind 和 lambda 的对比


7️⃣ 内置函数对象 — 算术、比较、逻辑

<functional> 头文件提供了用于常见操作的模板化函数对象。它们在 std::transformstd::sort 等算法中非常有用,通常比手写 lambda 更容易被编译器内联优化。

🔹 算术运算

函数对象 操作
std::plus<T> +
std::minus<T> -
std::multiplies<T> *
std::divides<T> /
std::modulus<T> %
std::negate<T> -(一元)

🔹 比较运算

函数对象 操作
std::equal_to<T> ==
std::not_equal_to<T> !=
std::greater<T> >
std::less<T> <
std::greater_equal<T> >=
std::less_equal<T> <=

🔹 逻辑运算

函数对象 操作
std::logical_and<T> &&
std::logical_or<T> `
std::logical_not<T> !

🔹 示例

#include <algorithm>
#include <functional>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {3, 1, 4, 1, 5};
    
    // 降序排序
    std::sort(v.begin(), v.end(), std::greater<int>());
    // v = {5,4,3,1,1}

    // 变换:每个元素乘以 2
    std::vector<int> result;
    std::transform(v.begin(), v.end(), std::back_inserter(result),
                   std::bind(std::multiplies<int>(), std::placeholders::_1, 2));
    // result = {10,8,6,2,2}

    // 使用 plus 进行累加
    int sum = std::accumulate(v.begin(), v.end(), 0, std::plus<int>());
    std::cout << sum << '\n'; // 14
}

8️⃣ 高级话题:std::function 与仅移动类型

std::function 要求存储的可调用对象必须是可拷贝构造的。对于仅移动类型(例如捕获了 std::unique_ptr 的 lambda),你需要其他方案——如 C++23 的 std::move_only_function 或自定义包装器。在 C++14/17 中,可使用 std::packaged_task 或设计自己的类型擦除接口。

// C++23 示例(尚未广泛支持)
#include <functional>
std::move_only_function<void()> f = [ptr = std::make_unique<int>(42)] { /*...*/ };

9️⃣ 性能与最佳实践

🔹 何时使用哪种工具

工具 适用场景
原始函数指针 极轻量、无捕获、固定签名
Lambda(auto) 大多数局部使用,最佳优化效果
std::function 存储异构可调用对象、回调、需要类型擦除
std::bind 遗留代码或高级完美转发场景
std::mem_fn 需要为成员函数创建可调用对象,尤其在泛型算法中
std::ref / std::cref 任何想避免拷贝到 bind/function 的场景
内置函数对象 标准操作,通常比 lambda 更快(编译器更了解它们)

🔹 避免不必要的 std::function

// ❌ 不好(无谓的开销)
void forEach(const std::vector<int>& v, std::function<void(int)> f) { ... }

// ✅ 好(模板,零开销)
template<typename F>
void forEach(const std::vector<int>& v, F f) { ... }

🔹 小对象优化(Small-Object Optimisation)

std::function 通常会内联存储小型可调用对象(例如无捕获的 lambda),避免堆分配。对于较大对象,则会在堆上分配。


🔟 完整实战示例 — 事件系统

让我们将所有内容整合到一个小型事件调度器中:

#include <functional>
#include <vector>
#include <map>
#include <iostream>

class EventDispatcher {
    using Handler = std::function<void(const std::string&, int)>;
    std::multimap<std::string, Handler> handlers;

public:
    void subscribe(const std::string& event, Handler h) {
        handlers.emplace(event, std::move(h));
    }

    void emit(const std::string& event, int value) {
        auto range = handlers.equal_range(event);
        for (auto it = range.first; it != range.second; ++it) {
            it->second(event, value);
        }
    }
};

struct Logger {
    void log(const std::string& ev, int val) const {
        std::cout << "[Logger] " << ev << " -> " << val << '\n';
    }
};

int main() {
    EventDispatcher dispatcher;

    // Lambda 处理器
    dispatcher.subscribe("click", [](const std::string& e, int v) {
        std::cout << "Lambda: " << e << " with " << v << '\n';
    });

    // 普通函数处理器
    auto freeHandler = [](const std::string& e, int v) {
        std::cout << "Free: " << e << " = " << v << '\n';
    };
    dispatcher.subscribe("update", freeHandler);

    // 使用 bind + ref 的成员函数处理器
    Logger logger;
    dispatcher.subscribe("log", std::bind(&Logger::log, &logger, 
                                          std::placeholders::_1, std::placeholders::_2));

    // 触发事件
    dispatcher.emit("click", 42);
    dispatcher.emit("update", 100);
    dispatcher.emit("log", 99);
}

输出:

Lambda: click with 42
Free: update = 100
[Logger] log -> 99

🎯 总结

#include <functional> 是现代 C++ 开发者不可或缺的工具。它提供了:

虽然 lambda 表达式已经取代了 std::bind 的许多用途,但 <functional> 头文件对于编写灵活、泛型且表达力强的 C++ 代码仍然不可或缺。明智地使用它,你的回调、算法和事件系统将变得既强大又清晰。