在 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 的类型表示“加法器”,而每个对象保存各自的加数。add10add20 使用相同的代码,却具有不同的行为:

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) {}

// 只读取成员状态,因此声明为 const
int operator()(int x) const {
return x + increment_;
}
};

int main() {
Adder add10{10};
Adder add20{20};

std::cout << add10(5) << '\n'; // 15
std::cout << add20(5) << '\n'; // 25
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); // 调用 int 版本
print(std::string_view{"hello"}); // 调用 string_view 版本
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'; // 1 2
std::cout << second() << '\n'; // 1
}

这类对象适合封装阈值、统计信息、重试策略、随机数引擎或预处理后的查找表。相比把这些数据放进全局变量,状态的所有权更清晰,测试时也更容易构造独立实例。

不过要特别注意:标准算法通常按值接收并可能复制仿函数。如果回调会修改内部状态,不要假设算法结束后原对象一定被修改;应优先把算法的返回值作为结果,或在确实需要共享同一对象时显式使用 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));
// 这里传入的是 counter 的引用包装,counter.count 为 3
}

这里的 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<>{});
// values: 12, 7, 5, 2

const auto it = std::find_if(values.begin(), values.end(), AtLeast{10});
return it != values.end() ? 0 : 1; // it 指向 12
}

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};

// [factor] 是捕获列表,捕获外部变量
std::for_each(nums.begin(), nums.end(), [factor](int n) {
std::cout << n * factor << " ";
});
// 输出: 2 4 6
return 0;
}

std::function (通用多态包装器)

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() {
// 1. 包装普通函数
std::function<int(int, int)> func = add;

// 2. 包装 Lambda
func = [](int a, int b) { return a * b; };

// 3. 包装仿函数
struct Divisor { int operator()(int a, int b) { return a / b; } };
func = Divisor();

std::cout << func(10, 2) << std::endl; // 调用的是最后赋值的 Divisor,输出 5
return 0;
}

类的成员函数指针 (Pointers to Member Functions)

这是一个比较特殊且语法晦涩的类别。非静态成员函数需要依赖一个对象实例才能调用。

  • 难点: 不能直接像 f() 那样调用,通常需要 (obj.*ptr)(args)(objptr->*ptr)(args)
  • 现代解法: 使用 std::mem_fnstd::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);

// 现代方式:使用 std::mem_fn
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> // std::invoke 所在头文件

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;

// 1. 调用普通函数
std::invoke(plainFunction, 10);

// 2. 调用 Lambda
auto lambda = [](int n) { std::println("Lambda: {}", n); };
std::invoke(lambda, 20);

// 3. 调用成员函数 (第一个参数是对象引用或指针)
std::invoke(&MyStruct::printSum, obj, 30);

// 4. 访问成员变量 (没错,成员变量也被视为“可调用”的,返回其值)
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::enable_if 或重载来区分情况
f(std::forward<Args>(args)...);
}

好品味的代码 (使用 std::invoke):

1
2
3
4
5
template <typename F, typename... Args>
auto callIt(F&& f, Args&&... args) {
// 无论 f 是什么,一把梭,全部搞定
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() {
// 将 tuple 中的 1, 2, 3 解包后传给 add
auto args = std::make_tuple(1, 2, 3);
int result = std::apply(add, args);
std::println("Result: {}", result); // 输出 6

// 配合 Lambda 使用
auto lambda = [](int x, int y) { return x * y; };
auto pair = std::make_pair(3, 4);
std::println("Lambda result: {}", std::apply(lambda, pair)); // 输出 12

return 0;
}

为什么需要它?

在没有 std::apply 之前,如果你想调用一个函数,但参数被包装在 tuple 里,你必须手动解包:

坏品味的代码 (手动解包):

1
2
3
4
5
6
template <typename F, typename Tuple>
auto callWithTuple(F f, const Tuple& t) {
// 假设 tuple 有 3 个元素,你必须这样写:
return f(std::get<0>(t), std::get<1>(t), std::get<2>(t));
// 如果 tuple 大小不确定?完蛋,模板元编程地狱等着你
}

好品味的代码 (使用 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 + ...); // C++17 折叠表达式
};

int main() {
// 从 vector 构造 tuple(需要编译期确定大小)
// 这里演示静态 tuple 的应用
auto values = std::make_tuple(1, 2, 3, 4, 5);
std::println("Sum: {}", std::apply(sum_all, values)); // 输出 15

// 配合成员函数使用
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)); // 输出 24

return 0;
}

std::invoke vs std::apply

特性std::invokestd::apply
参数传递方式直接传递参数从 tuple 解包传递
适用场景参数已知,需要统一调用语法参数被打包在 tuple 中
底层机制统一调用封装std::invoke + tuple 解包

简单来说:std::apply(f, args) 等价于 std::invoke(f, std::get<0>(args), std::get<1>(args), ...)