在现代 C++ 中,std::function(定义在 <functional> 头文件中)是一个通用的多态函数封装器,能够存储、复制和调用任何可调用对象 。你可以把它理解为一个“能够持有任何可调用对象”的容器。
在 std::function 出现之前,我们无法在一个容器或变量中存储不同类型的“可调用对象”。
- 普通函数指针只能存函数指针
- Lambda 的类型在编译期是唯一的匿名类型,甚至连两个写法相同的 Lambda 类型都不同
std::function 通过“类型擦除”技术,抹去了具体类型的差异,统一为 Ret(Args...) 的接口
一、定义
std::function 模板实例化了一个可以容纳任何满足以下条件的对象:
- 函数指针
- Lambda 表达式
- 函数对象(仿函数,重载了
operator() 的类)
- 成员函数指针(配合
std::bind 或 Lambda 使用)
1 2 3 4
| template<class R, class... Args> class function<R(Args...)> { };
|
注意,它要求可调用对象的拷贝是可用的,因为 std::function 通常存储其副本。
二、基本用法示例
2.1 普通函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <functional> #include <iostream>
int cook(int a, int b) { std::cout << a << " + " << b << " = " << a+b << std::endl; return a + b; }
int main() { std::function<int(int, int)> func = cook; func(3, 5);
return 0; }
|
2.2 函数指针
和普通函数一样:
1 2 3 4 5 6 7 8 9 10 11
| #include <functional> #include <iostream>
int main() { int (*funcPtr)(int,int) = cook; std::function<int(int,int)> func2 = funcPtr; func2(7, 2);
return 0; }
|
2.3 lambda 表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <functional> #include <iostream>
int main() { auto lambda = [](int a, int b) { std::cout << a << " * " << b << " = " << a * b << std::endl; return a * b; }; std::function<int(int, int)> func3 = lambda; func3(4, 6);
return 0; }
|
2.4 仿函数
只要一个类重载了 operator(),它的实例就是一个函数对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <functional> #include <iostream>
struct Multiplier { int operator()(int a, int b) const { std::cout << a << " * " << b << " = " << a * b << std::endl; return a * b; } };
int main() { Multiplier mul; std::function<int(int, int)> func4 = mul; func4(3, 7);
return 0; }
|
2.5 std::bind 产生的绑定表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <functional> #include <iostream>
int add(int a, int b, int c) { std::cout << a << " + " << b << " + " << c << " = " << a + b + c << std::endl; return a + b + c; }
int main() { auto bound = std::bind(add, 10, std::placeholders::_1, 20); std::function<int(int)> func5 = bound; func5(5);
return 0; }
|
std::bind 使用占位符来表示未绑定的参数,这些占位符决定了在生成的新函数对象中如何传递参数。
常见的占位符有:
std::placeholders::_1
std::placeholders::_2
std::placeholders::_3
- ……
2.6 成员函数指针
成员函数指针不能直接存进 std::function,因为它需要绑到一个对象上才能调用。
常见的包装方式有两种:std::bind 或 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
| #include <functional> #include <iostream>
struct Chef { void cookDinner(int count) const { std::cout << "count value: " << count << std::endl; } };
Chef myChef;
int main() { auto boundMember = std::bind(&Chef::cookDinner, &myChef, std::placeholders::_1); std::function<void(int)> func6 = boundMember; func6(3); std::function<void(int)> func7 = [&myChef](int count) { myChef.cookDinner(count); }; func7(5);
return 0; }
|
2.7 另一 function 对象
std::function 本身也是可拷贝的,所以我们可以把一个 std::function 赋值给另一个(只要签名相同)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <functional> #include <iostream>
struct Chef { void cookDinner(int count) const { std::cout << "count value: " << count << std::endl; } };
Chef myChef;
int main() { std::function<int(int, int)> original = cook; std::function<int(int, int)> copy = original; copy(2, 4);
return 0; }
|
三、性能优化
3.1 性能开销
虽然 std::function 非常方便,但它是有代价的:
- 堆分配:为了实现类型擦除,
std::function 内部通常会动态分配内存(取决于可调用对象的大小,虽然有小对象优化 SSO,但通常开销仍大于普通函数指针)
- 间接调用:由于它内部使用了虚函数或函数指针的封装,编译器很难对其进行内联优化,这会带来额外的开销
- 体积大:一个
std::function 对象通常占用 32~64 字节,而函数指针仅占用 8 字节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <iostream> #include <functional>
int main() { auto lambda1 = []() {}; std::function<void()> func = lambda1; std::cout << "lambda1 sizeof: " << sizeof(lambda1) << std::endl; std::cout << "func sizeof: " << sizeof(func) << std::endl;
return 0; }
|
3.2 使用 std::move 进行性能优化
在 C++ 中,std::function 的性能瓶颈主要在于其内存分配和对象拷贝。当我们将一个带有大量捕获变量(capture)的 Lambda 赋值给 std::function 时,编译器可能会在堆上分配内存来存储这些捕获的变量。
使用 std::move 来优化 std::function,核心思路是:**避免不必要的深拷贝,并利用移动语义将大对象的所有权转移进去。
移动语义可以让 std::function 在赋值或构造时转移所有权,而不是深拷贝内部资源。
尤其当内部有堆分配时,移动避免了昂贵的堆内存复制,只需交换指针。
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 33 34 35 36
| std::function<int(int)> createBigFun(bool useMove) { std::vector<int> huge(1000000); if (useMove) { return [huge = std::move(huge)](int x) { return x + huge[0]; }; } else { return [huge](int x) { return x + huge[0]; }; } }
int main() { auto start = std::chrono::steady_clock::now(); std::function<int(int)> f1 = createBigFun(false); auto end = std::chrono::steady_clock::now(); std::cout << "f1耗时: " << std::chrono::duration<double, std::milli>(end - start).count() << " ms" << std::endl;
start = std::chrono::steady_clock::now(); std::function<int(int)> f2 = createBigFun(true); end = std::chrono::steady_clock::now(); std::cout << "f2耗时: " << std::chrono::duration<double, std::milli>(end - start).count() << " ms" << std::endl; return 0; }
|
下面再看一下拷贝构造和移动构造的性能差别:
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 33 34 35 36 37 38 39 40 41 42 43
| std::function<int(int)> createBigFun(bool useMove) { std::vector<int> huge(1000000);
if (useMove) { return [huge = std::move(huge)](int x) { return x + huge[0]; }; } else { return [huge](int x) { return x + huge[0]; }; } }
int main() { std::function<int(int)> f1 = createBigFun(false);
auto start = std::chrono::steady_clock::now(); std::function<int(int)> f3 = f1; auto end = std::chrono::steady_clock::now(); std::cout << "拷贝耗时: " << std::chrono::duration<double, std::milli>(end - start).count() << " ms" << std::endl;
start = std::chrono::steady_clock::now(); std::function<int(int)> f4 = std::move(f1); end = std::chrono::steady_clock::now(); std::cout << "移动耗时: " << std::chrono::duration<double, std::milli>(end - start).count() << " ms" << std::endl;
return 0; }
|
- 拷贝:可能触发堆内存的完整复制,开销与对象大小成正比
- 移动:通常只是交换内部指针,常数时间,而且源对象变为空
3.3 使用场景
- 将
std::function 存入容器时:如果我们有一个 vector<std::function<void()>>,并且我们不再需要原始的 function 对象,用 emplace_back(std::move(func)) 或者 push_back(std::move(func)) 可以避免拷贝。
1 2 3
| std::vector<std::function<void()>> tasks; std::function<void()> task = []{ }; tasks.push_back(std::move(task));
|
作为函数参数传递时(尤其是回调):如果函数接受 std::function 作为参数,并且我们希望将所有权转移进去,用移动可以省去一次拷贝
1 2 3 4 5 6 7 8 9
| void enqueue(std::function<void()> task) { queue_.push_back(std::move(task)); }
std::function<void()> myTask = []{ doWork(); }; enqueue(std::move(myTask));
|
返回 std::function 时(已经自动优化):现代 C++ 编译器通常会应用 RVO(返回值优化) 或隐式移动。
当我们返回一个局部 std::function 时,一般不需要显式 std::move,显式写反而可能阻止某些优化。让编译器自己决定最优雅