前言
在 C++11 标准带来的诸多革命性特性中,简化代码编写与统一可调用对象管理是两大核心目标。Lambda 表达式解决了传统仿函数定义繁琐、复用性低的问题,让局部场景下的自定义逻辑能以更简洁的匿名函数形式实现;可变参数模板打破了模板参数数量固定的限制,为 STL 容器(如 emplace_back)和通用函数设计提供了灵活的参数处理能力;而 function 包装器与 bind 函数,则进一步整合了函数指针、仿函数、lambda 等不同类型的可调用对象,实现了统一管理与参数适配。
这些特性并非孤立存在。Lambda 的底层依赖仿函数实现,可变参数模板为 emplace 系列接口提供了技术支撑,function 与 bind 则基于前两者的特性,解决了不同可调用对象类型不统一的问题。本文将从实际开发需求出发,讲解 Lambda 表达式的语法规则与捕获逻辑,深入可变参数模板的展开方法与应用场景,并通过 function 包装器与 bind 函数的实例,展示如何统一管理各类可调用对象、灵活调整参数顺序与固定参数值。
文中将拆解易混淆的概念(如 Lambda 的 mutable 修饰符作用、placeholders 占位符的使用规则),并结合具体代码示例,帮助读者理解这些特性的设计逻辑与实际价值。
Lambda 表达式
Lambda 表达式是局部的匿名函数对象,其底层其实是用的仿函数实现的。它不存在重载的说法,即使两个 Lambda 看起来类型相同,它们也不是同一类型,不能相互赋值。
语法格式
[捕捉列表](参数列表)(mutable)-> 返回值类型 { 函数体 };
- 捕捉列表:可以捕捉该表达式所在域的局部变量。全局域里的不用捕获也能用。如果强行捕获可能会报错。
- 参数列表:如果不传参,
()可以省略。 - mutable:可以取消捕获列表里面东西默认的 const 性。在使用该修饰符的时候,参数列表不能省略。如果捕获的东西本来是 const 的话,加了 mutable 也变不了。
- 返回值类型:一般可以不用写,编译器会推导。
捕获列表规则
注意:如果没有使用
mutable,捕获的东西都不能被修改。
[var]:表示值传递方式捕捉变量 var,var 的类型会跟传递过来的一模一样。[=]:表示值传递方式捕获所有父作用域中的变量(包括 this)。[&var]:表示引用传递捕捉变量 var。[&]:表示引用传递捕捉所有父作用域中的变量(包括 this)。[&, x]:只有 x 是值传递,其他都是引用。- 注意:捕捉列表不允许变量重复传递,否则会导致编译错误(例如
[=, a]不行,a 捕获了两次)。
用法示例
#include <iostream>
#include <vector>
#include <functional>
int main() {
int x = 10;
// 值捕获
auto f1 = [x]() { return x; };
// 引用捕获
auto f2 = [&x]() { x++; };
std::cout << f1() << std::endl;
return 0;
}
可调用对象的四个存储方法:函数指针、仿函数、Lambda 表达式、用包装器搞到容器里面。
可变参数模板
可变参数模板让模板参数可以是不确定的数目。虽然日常自己写的话很少用,但在 STL 内部广泛使用。
基本语法
template<class... Args>
void ShowList(Args... args) {}
Args表示一个包含零个或多个类型的参数包。args也是参数包,里面可以是 0 或多个参数。...是展开参数包的操作(放在参数包后面,除了模板参数声明那里)。- 这个参数包想知道里面的参数是啥的话比较困难,不支持下标访问(eg:
arg[1]这样)。
展开参数包的方法
- 递归展开参数包。
- 逗号表达式展开参数包。
应用
构造函数转发
假设有个日期类 Date:
template<class... Args>
Date* Create(Args... args) {
Date* ret = new Date(args...);
return ret;
}
// 使用
Date* p1 = Create(2025, 9, 7);
// 或者
Date d(2025, 9, 7);
Date* p1 = Create(d);
emplace_back vs push_back
emplace_back有可变参数模板支持,可以直接传零散的参数进去。push_back没用可变参数模板,必须要先把零散的参数搞成临时对象再传进去。
包装器
Function 包装器
Function 包装器也叫作适配器,需要头文件 #include <functional>。它可以让可调用对象存储到容器中去了。
类模板原型
template<class T> class function;
template<class Ret, class... Args>
class function<Ret(Args...)>;
Ret:被调用函数的返回类型。Args...:被调用函数的形参。
使用示例
#include <functional>
#include <vector>
#include <map>
#include <string>
double f(double i) { return i / 2; }
struct Functor {
double operator()(double d) { return d / 3; }
};
int main() {
// vector 存储 function 对象
std::vector<std::function<double(double)>> v = {
f,
[](double d) -> double { return d / 4; },
Functor()
};
// map 存储 function 对象
std::map<std::string, std::function<int(int)>> map1 = {
{"a", [](int x) { return x; }},
{"y", [](int x) { return x + 1; }}
};
int x = 0;
int a = map1["a"](x);
return 0;
}
Bind 函数
Bind 是一个函数模板,接受一个可调用对象,生成一个新的可调用对象来'适应'原对象的参数列表。它的作用是把参数的顺序改了,并且可以固定一些参数的值。
固定参数值
缺省值只能给固定的参数一个固定值,而 bind 可以更灵活地处理参数位置。
#include <functional>
double Plus(int a, int b, double rate) {
return (a + b) * rate;
}
int main() {
// rate 固定是 4.0
std::function<double(int, int)> Plus1 = std::bind(
Plus,
std::placeholders::_2,
std::placeholders::_1,
4.0
);
// Plus1(9, 8) -> Plus(8, 9, 4.0)
return 0;
}
如果想固定的值在中间:
double Plus(int a, double rate, int b) {
return (a + b) * rate;
}
std::function<double(int, int)> Plus1 = std::bind(
Plus,
std::placeholders::_2,
4.0,
std::placeholders::_1
);
// Plus1(9, 8) -> 8 最终给了 a
类成员函数
- 如果函数是静态的,那到没啥区别(但是记得加上类域)。
- 如果函数不是静态的:函数后面要跟上这个对象或者对象的指针才行。
class SubType {
public:
int sub(int a, int b) { return a - b; }
};
SubType st;
// 绑定非静态成员函数
std::function<int(int, int)> Sub2 = std::bind(
&SubType::sub,
&st,
std::placeholders::_1,
std::placeholders::_2
);
// 也可以使用对象副本或匿名对象
std::function<int(int, int)> Sub3 = std::bind(
&SubType::sub,
SubType(),
std::placeholders::_1,
std::placeholders::_2
);
作业解析
关于范围 for 循环的说法,以下错误的是(C)。
A. 范围 for 可以直接应用在数组上 B. 对于 STL 提供的所有容器,均可以使用范围 for 依次访问其元素 C. 使用范围 for 操作 stack,可以简化代码 D. 对于自定义类型,想要支持范围 for,必须提供 begin 和 end 迭代器 E. 范围 for 编译器最终是将其转化为迭代器来进行处理的
解析:选项 C 错误。上面说的是容器,但是 stack 是容器适配器,不支持直接的范围 for 遍历。


