
在 C++ 中,Callable Object(可调用对象) 指的是任何可以像函数一样被调用的事物。简单来说,就是任何可以使用 () 运算符(Function Call Operator)的对象。
在现代 C++(C++11 及以后)中,可调用对象的概念非常重要,因为它是 STL 算法、线程(std::thread)以及回调机制的核心。
C++ 中的可调用对象主要分为以下 5 类。
普通函数与函数指针 (Function Pointers)
这是最基础的形式,继承自 C 语言。
- 特点: 没有状态(Stateless),行为固定。
- 适用场景: 简单的回调,也就是所谓的 C 风格 API。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <print>
void hello() { std::println("Hello from Function Pointer!"); }
int main() { void (*funcPtr)() = &hello; funcPtr(); return 0; }
|
Godbolt
仿函数 (Functors / Function Objects)
仿函数是重载了函数调用运算符 operator() 的对象,也常被称为函数对象(Function Object)。因此,下面两种写法表达的是同一次调用:
1 2
| adder(5); adder.operator()(5);
|
它不是“长得像函数的特殊语法”,而是一个普通对象:可以有构造函数、成员变量、成员函数,也可以为 operator() 做重载。函数指针只描述“调用哪段代码”,而仿函数同时描述“调用哪段代码”和“以什么配置或状态调用”。
最小示例:把配置封装进对象
下面的 Adder 的类型表示“加法器”,而每个对象保存各自的加数。add10 与 add20 使用相同的代码,却具有不同的行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <iostream>
class Adder { int increment_;
public: explicit Adder(int increment) : increment_(increment) {}
int operator()(int x) const { return x + increment_; } };
int main() { Adder add10{10}; Adder add20{20};
std::cout << add10(5) << '\n'; std::cout << add20(5) << '\n'; return 0; }
|
这正是 Lambda 捕获的具名版本。大致可以把 [increment](int x) { return x + increment; } 理解成编译器生成了一个含有 increment 成员和 operator() 的匿名类。
operator() 能像普通成员函数一样设计
仿函数不受“一个类型只能有一种调用方式”的限制。可以按参数重载,也可以把它写成模板,从而接受多种类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <iostream> #include <string_view>
struct Printer { void operator()(int value) const { std::cout << "integer: " << value << '\n'; }
void operator()(std::string_view value) const { std::cout << "text: " << value << '\n'; }
template <typename T> void operator()(const T& value) const { std::cout << "other: " << value << '\n'; } };
int main() { Printer print; print(42); print(std::string_view{"hello"}); print(3.14); }
|
如果调用不应修改对象,就应将 operator() 声明为 const。这让临时对象、const 对象和大多数只读的算法回调都能使用它;只有确实需要在调用之间更新内部状态时,才省略 const。
有状态仿函数:状态属于对象实例
下面的计数器展示了仿函数最直接的价值:调用后的状态会保留在同一个对象中。两个对象互不影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <iostream>
class CallCounter { int count_ = 0;
public: int operator()() { return ++count_; } };
int main() { CallCounter first; CallCounter second;
std::cout << first() << ' ' << first() << '\n'; std::cout << second() << '\n'; }
|
这类对象适合封装阈值、统计信息、重试策略、随机数引擎或预处理后的查找表。相比把这些数据放进全局变量,状态的所有权更清晰,测试时也更容易构造独立实例。
不过要特别注意:标准算法通常按值接收并可能复制仿函数。如果回调会修改内部状态,不要假设算法结束后原对象一定被修改;应优先把算法的返回值作为结果,或在确实需要共享同一对象时显式使用 std::ref。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <algorithm> #include <functional> #include <vector>
struct Counter { int count = 0; void operator()(int) { ++count; } };
int main() { std::vector<int> values{1, 2, 3}; Counter counter;
std::for_each(values.begin(), values.end(), std::ref(counter)); }
|
这里的 std::ref 并不复制 counter 本身,而是让算法调用原对象。若算法可能并行执行,还必须额外保证这份可变状态的线程安全。
在 STL 算法中作为谓词或策略
标准库大量使用小型仿函数。例如比较器、筛选条件和数值操作都可以作为算法参数。为常见操作手写类型通常没有必要,<functional> 已经提供了 std::less<>、std::greater<>、std::plus<> 等仿函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <algorithm> #include <functional> #include <vector>
struct AtLeast { int threshold;
bool operator()(int value) const { return value >= threshold; } };
int main() { std::vector<int> values{7, 2, 12, 5};
std::sort(values.begin(), values.end(), std::greater<>{});
const auto it = std::find_if(values.begin(), values.end(), AtLeast{10}); return it != values.end() ? 0 : 1; }
|
std::greater<> 末尾的空模板参数很有用:它是透明仿函数(C++14 起),可让参数类型由调用点推导;相较于 std::greater<int>,在泛型代码和异构查找中更灵活。
性能与选型
仿函数的具体类型通常在编译期已知,优化器因而有机会内联 operator()、传播其成员中的常量,甚至完全消除对象本身。函数指针或 std::function 的目标若只在运行期确定,这类优化往往更困难;但是否内联仍取决于编译器、优化级别和实际调用路径,不能把“仿函数一定更快”当作规则。
实践中可以这样选择:
- 局部且一次性的逻辑,优先使用 Lambda,代码离调用点最近。
- 逻辑需要命名、复用、单独测试,或有复杂状态与多个
operator() 重载时,定义仿函数类型。 - 需要在运行期保存不同类型、但签名相同的回调时,使用
std::function。 - 需要兼容 C 风格接口时,使用普通函数或函数指针。
因此,仿函数并不是 Lambda 的竞争品;Lambda 更适合短小的匿名函数对象,仿函数则适合需要成为领域模型一部分的、具名且可复用的可调用类型。
Lambda 表达式 (Lambda Expressions)
引入于 C++11,Lambda 是现代 C++ 最常用的可调用对象。
- 本质: Lambda 实际上是匿名仿函数的语法糖。编译器在幕后为你生成了一个重载了
operator() 的类。 - 特点: 代码紧凑,可以直接在调用点定义。
- 捕获列表
[]: 允许你捕获上下文中的变量(按值或按引用),这对应仿函数的成员变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <iostream> #include <vector> #include <algorithm>
int main() { int factor = 2; std::vector<int> nums = {1, 2, 3};
std::for_each(nums.begin(), nums.end(), [factor](int n) { std::cout << n * factor << " "; }); return 0; }
|
std::function 是 C++11 引入的一个标准库模板类,位于 <functional> 头文件。
- 特点: 它是一个类型擦除(Type Erasure)的容器。它可以存储任何符合特定签名的可调用对象(函数指针、仿函数、Lambda 等)。
- 代价: 由于使用了虚函数机制和可能的堆内存分配,它比直接使用 Lambda 或模板有轻微的性能开销。
- 适用场景: 当你需要存储不同类型的回调,或者作为函数参数不需要模板化时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <iostream> #include <functional>
int add(int a, int b) { return a + b; }
int main() { std::function<int(int, int)> func = add; func = [](int a, int b) { return a * b; }; struct Divisor { int operator()(int a, int b) { return a / b; } }; func = Divisor();
std::cout << func(10, 2) << std::endl; return 0; }
|
类的成员函数指针 (Pointers to Member Functions)
这是一个比较特殊且语法晦涩的类别。非静态成员函数需要依赖一个对象实例才能调用。
- 难点: 不能直接像
f() 那样调用,通常需要 (obj.*ptr)(args) 或 (objptr->*ptr)(args)。 - 现代解法: 使用
std::mem_fn 或 std::bind(虽已过时),或者在 C++17 中使用 std::invoke。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <iostream> #include <functional>
class Foo { public: void print(int x) { std::cout << "Foo: " << x << std::endl; } };
int main() { Foo obj; void (Foo::*ptr)(int) = &Foo::print;
(obj.*ptr)(42);
auto runnable = std::mem_fn(&Foo::print); runnable(obj, 42); return 0; }
|
总结与对比
为了让你更直观地理解,我做了一个对比表:
| 类型 | 是否有状态 | 灵活性 | 性能 | 典型用途 |
|---|
| 函数指针 | 无 | 低 | 高 (但在内联方面不如仿函数) | C 接口兼容,简单的全局回调 |
| 仿函数 | 有 | 中 | 极高 (易被编译器内联) | 需要状态的复杂逻辑,STL 算法 |
| Lambda | 有 (通过捕获) | 高 | 极高 (同仿函数) | 绝大多数现代 C++ 场景,局部逻辑 |
| std::function | 有 | 极高 (可存任何类型) | 中 (虚函数开销,堆分配) | API 接口设计,存储异构回调列表 |
| 成员函数指针 | 依赖对象 | 低 | 高 | 特定类操作,通常配合 bind/mem_fn 使用 |
std::invoke (C++17)
由于上面提到的调用方式五花八门(有的直接用 (),有的要用 .*),C++17 引入了 std::invoke 来统一所有可调用对象的调用语法。
统一调用语法
std::invoke 的强大之处在于它抹平了普通函数、Lambda、成员函数甚至成员变量之间的调用差异。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream> #include <functional>
struct MyStruct { int value{42}; void printSum(int n) const { std::println("Sum: {}", value + n); } };
void plainFunction(int n) { std::println("Plain: {}", n); }
int main() { MyStruct obj;
std::invoke(plainFunction, 10);
auto lambda = [](int n) { std::println("Lambda: {}", n); }; std::invoke(lambda, 20);
std::invoke(&MyStruct::printSum, obj, 30);
std::println("Member value: {}", std::invoke(&MyStruct::value, obj));
return 0; }
|
为什么需要它?(泛型编程的神器)
如果你在编写一个模板函数,需要接受一个“可调用对象”并执行它,在没有 std::invoke 之前,你很难处理成员函数指针。
坏品味的代码 (C++11 以前):
1 2 3 4 5 6
| template <typename F, typename... Args> void callIt(F f, Args&&... args) { f(std::forward<Args>(args)...); }
|
好品味的代码 (使用 std::invoke):
1 2 3 4 5
| template <typename F, typename... Args> auto callIt(F&& f, Args&&... args) { return std::invoke(std::forward<F>(f), std::forward<Args>(args)...); }
|
在现代 C++ 的库开发中,std::invoke 是实现高阶函数(如 std::thread 的构造函数、std::async 等)的基石。它让代码更简洁,消除了不必要的特殊情况处理。
std::apply (C++17)
如果说 std::invoke 解决了"如何统一调用"的问题,那么 std::apply 解决的就是"如何将 tuple 展开为参数"的问题。
核心功能
std::apply 接受一个可调用对象和一个 tuple,将 tuple 中的元素解包后作为参数传递给可调用对象。这本质上就是 std::invoke + tuple 解包的组合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <iostream> #include <functional> #include <tuple>
int add(int a, int b, int c) { return a + b + c; }
int main() { auto args = std::make_tuple(1, 2, 3); int result = std::apply(add, args); std::println("Result: {}", result);
auto lambda = [](int x, int y) { return x * y; }; auto pair = std::make_pair(3, 4); std::println("Lambda result: {}", std::apply(lambda, pair));
return 0; }
|
为什么需要它?
在没有 std::apply 之前,如果你想调用一个函数,但参数被包装在 tuple 里,你必须手动解包:
坏品味的代码 (手动解包):
1 2 3 4 5 6
| template <typename F, typename Tuple> auto callWithTuple(F f, const Tuple& t) { return f(std::get<0>(t), std::get<1>(t), std::get<2>(t)); }
|
好品味的代码 (使用 std::apply):
1 2 3 4
| template <typename F, typename Tuple> auto callWithTuple(F&& f, Tuple&& t) { return std::apply(std::forward<F>(f), std::forward<Tuple>(t)); }
|
实际应用场景
std::apply 在处理动态参数列表时特别有用,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <iostream> #include <tuple> #include <vector> #include <numeric>
auto sum_all = [](auto... args) { return (args + ...); };
int main() { auto values = std::make_tuple(1, 2, 3, 4, 5); std::println("Sum: {}", std::apply(sum_all, values));
struct Calculator { int multiply(int a, int b, int c) { return a * b * c; } }; Calculator calc; auto member_args = std::make_tuple(&calc, 2, 3, 4); std::println("Product: {}", std::apply(&Calculator::multiply, member_args));
return 0; }
|
std::invoke vs std::apply
| 特性 | std::invoke | std::apply |
|---|
| 参数传递方式 | 直接传递参数 | 从 tuple 解包传递 |
| 适用场景 | 参数已知,需要统一调用语法 | 参数被打包在 tuple 中 |
| 底层机制 | 统一调用封装 | std::invoke + tuple 解包 |
简单来说:std::apply(f, args) 等价于 std::invoke(f, std::get<0>(args), std::get<1>(args), ...)。