开篇介绍:
hello 大家,本篇博客我们来学习C++11中的lambda、function、bind。
在 C++11 标准诞生之前,开发者在处理轻量级函数逻辑、统一可调用对象类型、调整函数参数格式等场景时,往往面临诸多掣肘:实现一个简单的排序规则需要定义一个仿函数类,存储不同类型的可调用对象需要复杂的模板特化,调整函数参数顺序则需要重写函数重载版本。这些繁琐的操作不仅增加了代码的冗余度,还降低了开发效率与可读性。
C++11 引入的 Lambda 表达式、std::function 包装器与 std::bind 适配器,彻底改变了这一局面。它们作为现代 C++ 中处理可调用对象的核心工具,分别从轻量级函数定义、可调用对象类型统一、函数参数适配三个维度,为开发者提供了简洁、灵活、高效的解决方案。
一、Lambda 表达式:轻量级匿名函数对象的优雅实现
Lambda 表达式是 C++11 引入的匿名函数对象,其核心优势在于可以直接定义在函数内部,无需像普通函数那样在全局或命名空间中声明,同时支持捕捉外层作用域的变量,极大地提升了代码的简洁性与灵活性。从底层原理来看,Lambda 表达式并非一种全新的语言特性,而是编译器自动生成的仿函数类的 “语法糖”,但这层语法糖却让开发者摆脱了手动定义仿函数的繁琐流程。
1.1 Lambda 表达式的完整语法结构
Lambda 表达式的标准语法格式为:[capture-list] (parameters) -> return-type { function-body }。这五个部分各司其职,共同构成了一个完整的 Lambda 表达式,其中部分组件在满足特定条件时可以省略。
1.1.1 捕捉列表 [capture-list]:Lambda 的核心标识与变量捕捉入口
捕捉列表是 Lambda 表达式的起始标识,也是其最核心、最易出错的部分,其作用是捕捉 Lambda 定义位置的外层作用域局部变量,供函数体内部使用。捕捉列表的使用遵循严格的规则,具体可分为基础规则与捕捉方式两大模块。
基础规则
- 捕捉列表必须作为 Lambda 表达式的开头,哪怕不需要捕捉任何变量,空的 [] 也不能省略 —— 这是编译器识别 Lambda 表达式的关键标志。
- 捕捉列表只能捕捉 Lambda 定义位置之前 的局部变量,无法捕捉定义之后声明的局部变量。这是因为编译器的编译流程是自上而下的,后续声明的变量在 Lambda 定义时还未进入作用域。
- 静态局部变量和全局变量不需要捕捉,可直接在 Lambda 函数体中使用。原因在于静态局部变量的生命周期贯穿整个程序运行期,作用域覆盖其定义位置之后的所有代码;全局变量的作用域则覆盖整个程序,二者都不受局部作用域的限制。
- 如果 Lambda 表达式定义在全局作用域(而非函数内部),捕捉列表必须为空。因为全局作用域不存在 “外层局部变量”,没有可捕捉的对象。
三种捕捉方式捕捉列表支持三种捕捉方式,分别对应不同的使用场景,开发者可根据需求灵活选择。
方式一:显式捕捉 —— 精准控制单个变量的访问权限显式捕捉是指在捕捉列表中明确指定每个变量的捕捉类型,多个变量之间用逗号分隔。这种方式的优势在于可以精准控制每个变量的访问权限,适用于需要对不同变量采取不同捕捉策略的场景。显式捕捉的核心语法规则是:变量名前不加 & 表示值捕捉,加 & 表示引用捕捉。
- 值捕捉的本质是拷贝外层变量的副本到 Lambda 表达式内部。由于是副本,Lambda 函数体内部对该变量的修改不会影响外层原变量。同时,值捕捉的变量默认是只读的,无法在函数体内部修改 —— 这是因为编译器生成的仿函数类中,值捕捉的变量会被声明为普通成员变量,而仿函数的 operator () 运算符默认是 const 成员函数,const 成员函数无法修改类的普通成员变量。
- 引用捕捉的本质是为外层变量建立一个引用别名,Lambda 函数体内部对该变量的所有操作,都是直接作用于外层原变量。因此,引用捕捉的变量可以直接修改,且修改结果会同步到外层作用域。
需要注意的是,显式捕捉的变量必须是 Lambda 定义位置之前已声明的局部变量,未被捕捉的变量若在函数体中使用,会直接触发编译错误。
代码示例:显式捕捉
#include <iostream>
using namespace std;
int main() {
int a = 10;
int b = 20;
// 显式值捕捉 a,引用捕捉 b
auto lambda = [a, &b]() {
// a = 100; // 编译错误:值捕捉的变量默认只读
b = 200; // 引用捕捉的变量可修改,会影响外层变量
cout << "Lambda 内部: a = " << a << ", b = " << b << endl;
};
lambda();
cout << "Lambda 外部: a = " << a << ", b = " << b << endl; // a=10, b=200
return 0;
}
方式二:隐式捕捉 —— 编译器自动捕捉所需变量当需要捕捉的变量数量较多,且所有变量的捕捉方式一致时,显式捕捉会显得冗余。此时可以使用隐式捕捉,即通过 = 或 & 指定默认捕捉规则,编译器会自动捕捉函数体中用到的所有外层局部变量,未用到的变量不会被捕捉,从而避免不必要的性能开销。隐式捕捉的核心语法规则是:
- [=] 表示隐式值捕捉,函数体中用到的所有外层局部变量,都以值捕捉的方式获取副本。值捕捉的变量同样默认只读,无法在函数体内部修改。
- [&] 表示隐式引用捕捉,函数体中用到的所有外层局部变量,都以引用捕捉的方式关联原变量。引用捕捉的变量可以直接修改,且修改结果会同步到外层作用域。
隐式捕捉的优势在于简化代码书写,同时保证捕捉的精准性 —— 编译器只会捕捉函数体中实际用到的变量,不会多余捕捉未使用的变量。
代码示例:隐式捕捉
#include <iostream>
using namespace std;
int main() {
int x = 5, y = 8, z = 10;
// 隐式值捕捉:只捕捉用到的 x 和 y,z 不会被捕捉
auto lambda1 = [=]() {
// x = 100; // 编译错误:值捕捉默认只读
cout << "lambda1: x = " << x << ", y = " << y << endl;
};
// 隐式引用捕捉:修改会影响外层变量
auto lambda2 = [&]() {
x = 100;
y = 200;
cout << "lambda2: x = " << x << ", y = " << y << endl;
};
lambda1(); // 输出:lambda1: x = 5, y = 8
lambda2(); // 输出:lambda2: x = 100, y = 200
cout << "外部: x = " << x << ", y = " << y << ", z = " << z << endl; // x=100, y=200, z=10
return 0;
}
方式三:混合捕捉 —— 隐式与显式的结合使用在实际开发中,常常会遇到 “大部分变量采用同一种捕捉方式,个别变量需要特殊处理” 的场景,此时可以使用混合捕捉,即隐式捕捉与显式捕捉结合使用。混合捕捉遵循严格的语法规则,若违反规则会直接触发编译错误。混合捕捉的核心规则:
- 捕捉列表的第一个元素必须是 = 或 &,用于指定默认的捕捉方式。
- 若默认捕捉方式是 =(隐式值捕捉),后续显式指定的变量必须采用引用捕捉(变量名前加 &),不能再使用值捕捉。这是为了避免语法歧义,明确区分默认捕捉与特殊捕捉的变量。
- 若默认捕捉方式是 &(隐式引用捕捉),后续显式指定的变量必须采用值捕捉(变量名前不加 &),不能再使用引用捕捉。
例如,[=, &a] 表示默认对所有用到的变量进行值捕捉,唯独变量 a 采用引用捕捉;[&, b] 表示默认对所有用到的变量进行引用捕捉,唯独变量 b 采用值捕捉。混合捕捉兼顾了代码的简洁性与变量控制的精准性,是实际开发中使用频率较高的捕捉方式。
代码示例:混合捕捉
#include <iostream>
using namespace std;
int main() {
int m = 1, n = 2, p = 3;
// 默认值捕捉,p 单独引用捕捉
auto lambda1 = [=, &p]() {
// m = 10; // 编译错误:默认值捕捉只读
p = 30; // p 是引用捕捉,可修改
cout << "lambda1: m = " << m << ", n = " << n << ", p = " << p << endl;
};
// 默认引用捕捉,n 单独值捕捉
auto lambda2 = [&, n]() {
m = 10; // 引用捕捉可修改
// n = 20; // 编译错误:n 是值捕捉只读
cout << "lambda2: m = " << m << ", n = " << n << ", p = " << p << endl;
};
lambda1(); // 输出:lambda1: m = 1, n = 2, p = 30
lambda2(); // 输出:lambda2: m = 10, n = 2, p = 30
cout << "外部: m = " << m << ", n = " << n << ", p = " << p << endl; // m=10, n=2, p=30
return 0;
}
1.1.2 参数列表 (parameters):与普通函数一致的参数接收入口
参数列表的作用是接收外部传入的参数,其语法格式与普通函数的参数列表完全一致,支持普通参数、默认参数、可变参数等特性。参数列表的省略规则如下:
- 如果 Lambda 表达式不需要接收外部参数,参数列表可以连同括号 () 一起省略。例如,一个简单的打印函数可以写成 [] { cout << "Hello Lambda" << endl; }。
- 如果 Lambda 表达式使用了 mutable 修饰符,即使没有参数,参数列表的括号 () 也不能省略。这是 mutable 修饰符的强制要求,目的是明确区分修饰符的作用范围。
代码示例:参数列表使用
#include <iostream>
using namespace std;
int main() {
// 省略参数列表:无参数
auto print = [] { cout << "Hello Lambda!" << endl; };
print();
// 带参数列表:普通参数 + 默认参数
auto add = [](int a, int b = 10) { return a + b; };
cout << "add(5) = " << add(5) << endl; // 输出:15
cout << "add(5, 20) = " << add(5, 20) << endl; // 输出:25
return 0;
}
1.1.3 返回值类型 -> return-type:可推导的类型声明
返回值类型用于指定 Lambda 表达式的返回值类型,采用 C++11 引入的 “追踪返回类型” 语法,即返回值类型写在参数列表之后,以 -> 开头。返回值类型的省略规则较为灵活,具体如下:
- 如果 Lambda 表达式没有返回值(函数体中无 return 语句,或 return 语句无返回值),返回值类型部分可以直接省略,编译器会默认将返回值类型推导为 void。
- 如果 Lambda 表达式有返回值,且所有 return 语句的返回类型一致,返回值类型部分可以省略,编译器会自动推导返回值类型。例如,[](int a, int b) { return a + b; } 会被编译器自动推导为返回 int 类型。
- 如果 Lambda 表达式有多个 return 语句,且返回类型不同(例如一个 return 返回 int 类型,另一个 return 返回 double 类型),必须显式指定返回值类型,否则编译器无法完成类型推导,会触发编译错误。
代码示例:返回值类型推导
#include <iostream>
using namespace std;
int main() {
// 无返回值:省略返回值类型,默认 void
auto func1 = []() { cout << "无返回值 Lambda" << endl; };
func1();
// 单一返回类型:省略返回值类型,自动推导为 int
auto func2 = [](int a, int b) { return a * b; };
cout << "func2(3,4) = " << func2(3,4) << endl; // 输出:12
// 多返回类型:必须显式指定返回值类型
auto func3 = [](int a) -> double {
if (a > 0) return 3.14;
else return 10; // 10 会被转换为 double 类型
};
cout << "func3(5) = " << func3(5) << endl; // 输出:3.14
cout << "func3(-5) = " << func3(-5) << endl; // 输出:10
return 0;
}
1.1.4 函数体 {function-body}:Lambda 的核心逻辑实现
函数体是 Lambda 表达式的核心逻辑所在,其语法格式与普通函数的函数体完全一致,支持变量定义、循环、分支、函数调用等所有普通函数支持的语法结构。函数体的使用规则如下:
- 函数体中可以使用的变量包括:参数列表中的参数、捕捉列表中捕捉的变量、全局变量与静态局部变量。
- 即使函数体为空,大括号 {} 也不能省略。这是 Lambda 表达式语法结构的强制要求,目的是明确函数体的边界。
代码示例:函数体使用
#include <iostream>
#include <vector>
using namespace std;
int global_var = 100; // 全局变量
int main() {
static int static_var = 200; // 静态局部变量
vector<int> vec{1,2,3,4,5};
// 函数体包含循环、分支、全局/静态变量使用
auto process = [vec]() {
int sum = 0;
for (int num : vec) {
if (num % 2 == 0) {
sum += num + global_var + static_var;
}
}
cout << "sum = " << sum << endl;
};
process(); // 输出:sum = (2+4) + 100*2 + 200*2 = 6 + 200 + 400 = 606
return 0;
}
1.2 关键修饰符:mutable—— 解除值捕捉的只读限制
mutable 是 Lambda 表达式的可选修饰符,其核心作用是取消值捕捉变量的 const 属性,允许在函数体中修改值捕捉的变量。mutable 修饰符的使用规则与注意事项如下:
- 作用范围:mutable 仅对值捕捉的变量有效,对引用捕捉的变量无影响。因为引用捕捉的变量本身就可以直接修改,不需要额外的修饰符。
- 核心注意点:mutable 允许修改的是值捕捉的副本,不会影响外层原变量。这是因为值捕捉的本质是拷贝,副本与原变量是两个相互独立的对象。例如,外层有一个变量 int a = 10,Lambda 表达式 a mutable {a = 20;} 调用后,副本 a 的值变为 20,但外层原变量 a 的值仍然是 10。
- 语法要求:使用 mutable 修饰符时,参数列表的括号 () 不能省略,即使 Lambda 表达式没有参数。
代码示例:mutable 修饰符
#include <iostream>
using namespace std;
int main() {
int val = 10;
// mutable 修饰:允许修改值捕捉的副本
auto lambda = [val]() mutable {
val = 100;
cout << "Lambda 内部 val = " << val << endl; // 输出:100
};
lambda();
cout << "Lambda 外部 val = " << val << endl; // 输出:10(原变量未变)
return 0;
}
1.3 Lambda 表达式的底层原理:编译器生成的匿名仿函数
从语法层面看,Lambda 表达式是一种简洁的函数定义方式;但从底层实现来看,Lambda 表达式的本质是编译器自动生成的匿名仿函数类。当开发者编写一个 Lambda 表达式时,编译器会在编译阶段自动完成以下工作:
当 Lambda 表达式使用 mutable 修饰符时,编译器会去掉 operator () 运算符的 const 属性,从而允许修改值捕捉的成员变量。这一底层原理揭示了 Lambda 表达式与仿函数的本质联系,也解释了 Lambda 表达式的所有语法规则与特性。
代码示例:Lambda 与仿函数等价性
#include <iostream>
using namespace std;
// 手动实现的仿函数(等价于编译器为 Lambda 生成的类)
class MyLambda {
private:
int a; // 值捕捉的变量 -> 普通成员变量
int& b; // 引用捕捉的变量 -> 引用成员变量
public:
// 构造函数:初始化捕捉的变量
MyLambda(int a_, int& b_) : a(a_), b(b_) {}
// operator():对应 Lambda 的函数体
void operator()() const { // 默认 const,对应值捕捉只读
// a = 100; // 编译错误:const 成员函数不能修改普通成员变量
b = 200;
cout << "仿函数: a = " << a << ", b = " << b << endl;
}
};
int main() {
int x = 10, y = 20;
// 手动仿函数调用
MyLambda func(x, y);
func();
// 等价的 Lambda 表达式
auto lambda = [x, &y]() {
// x = 100; // 编译错误:值捕捉默认只读
y = 200;
cout << "Lambda: x = " << x << ", y = " << y << endl;
};
lambda();
return 0;
}
1.4 Lambda 表达式的核心应用场景
Lambda 表达式的出现,极大地简化了 C++ 中轻量级函数逻辑的实现,其核心应用场景主要包括以下几个方面:
1.4.1 STL 算法的自定义逻辑
STL 中的许多算法(如 sort、find_if、for_each 等)都支持传入自定义逻辑,用于实现个性化的排序、查找、遍历等功能。在 Lambda 表达式出现之前,实现这些自定义逻辑需要手动定义仿函数类,代码冗余且可读性差。Lambda 表达式则可以直接在算法参数中定义自定义逻辑,极大地简化了代码书写。
代码示例:STL 算法中使用 Lambda
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 商品结构体
struct Goods {
string name;
double price;
Goods(string n, double p) : name(n), price(p) {}
};
int main() {
vector<Goods> goodsList{
{"苹果", 5.99},
{"香蕉", 3.50},
{"橙子", 4.80},
{"葡萄", 8.99}
};
// 按价格升序排序:直接使用 Lambda 作为排序规则
sort(goodsList.begin(), goodsList.end(),
[](const Goods& g1, const Goods& g2) {
return g1.price < g2.price;
});
// 遍历输出
cout << "按价格升序排序:" << endl;
for_each(goodsList.begin(), goodsList.end(),
[](const Goods& g) {
cout << g.name << ": " << g.price << endl;
});
// 查找价格大于 5 的商品
auto it = find_if(goodsList.begin(), goodsList.end(),
[](const Goods& g) {
return g.price > 5.0;
});
if (it != goodsList.end()) {
cout << "\\n第一个价格大于 5 的商品:" << it->name << endl;
}
return 0;
}
1.4.2 线程函数的自定义执行逻辑
在多线程编程中,创建线程时需要传入线程的执行函数。使用普通函数作为线程函数时,无法直接使用外层函数的局部变量;使用仿函数则需要定义额外的类。Lambda 表达式可以通过捕捉列表直接捕捉外层局部变量,自定义线程的执行逻辑,无需额外定义类或函数。
代码示例:线程中使用 Lambda
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
int main() {
vector<thread> threads;
int threadCount = 3;
// 创建多个线程,每个线程打印指定次数的信息
for (int i = 0; i < threadCount; ++i) {
threads.emplace_back([i]() { // 捕捉当前线程 ID
for (int j = 0; j < 3; ++j) {
cout << "Thread " << i << " is running: " << j << endl;
// 模拟耗时操作
this_thread::sleep_for(chrono::milliseconds(100));
}
});
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
return 0;
}
1.4.3 智能指针的自定义删除器
C++ 中的智能指针(如 unique_ptr、shared_ptr)默认使用 delete 运算符释放资源,但对于一些特殊资源(如动态数组、文件指针、网络连接等),需要自定义删除器。Lambda 表达式可以简洁地实现自定义删除器,无需定义额外的函数或仿函数。
代码示例:智能指针自定义删除器
#include <iostream>
#include <memory>
#include <cstdio>
using namespace std;
int main() {
// 1. 管理动态数组的 unique_ptr
unique_ptr<int, function<void(int*)>> arrPtr(
new int[5]{1,2,3,4,5},
[](int* p) { // Lambda 作为删除器
cout << "释放动态数组…" << endl;
delete[] p;
}
);
// 2. 管理文件指针的 shared_ptr
FILE* fp = fopen("test.txt", "w");
if (fp) {
shared_ptr<FILE> filePtr(
fp,
[](FILE* p) { // Lambda 作为删除器
cout << "关闭文件…" << endl;
fclose(p);
}
);
fprintf(filePtr.get(), "Hello Lambda!");
}
return 0;
}
1.4.4 回调函数的简化实现
在异步编程或事件驱动编程中,回调函数是一种常用的编程模式。使用普通函数作为回调函数时,传递参数较为繁琐;使用仿函数则需要定义额外的类。Lambda 表达式可以通过捕捉列表直接捕捉所需的参数,简化回调函数的实现。
代码示例:回调函数中使用 Lambda
#include <iostream>
#include <string>
#include <functional>
using namespace std;
// 模拟用户数据
struct UserData {
string userId;
string userName;
};
// 模拟响应数据
struct Response {
int code;
string data;
};
// 模拟异步请求函数
void asyncRequest(const string& url, function<void(const Response&)> callback) {
// 模拟网络请求耗时
this_thread::sleep_for(chrono::milliseconds(500));
// 构造响应数据
Response resp{200, "Success: " + url};
// 调用回调函数
callback(resp);
}
// 处理响应
void processResponse(const UserData& user, const Response& resp) {
cout << "用户 " << user.userId << "(" << user.userName << ") 的请求结果:" << endl;
cout << "状态码:" << resp.code << ", 数据:" << resp.data << endl;
}
int main() {
UserData user{"1001", "张三"};
string url = "https://example.com/api/data";
// Lambda 作为回调函数,捕捉 user 变量
asyncRequest(url, [user](const Response& resp) {
processResponse(user, resp);
});
// 等待回调执行完成
this_thread::sleep_for(chrono::seconds(1));
return 0;
}
二、std::function 包装器:可调用对象的类型统一神器
在 C++ 中,可调用对象是一个广义的概念,包括普通函数、函数指针、仿函数、Lambda 表达式、类成员函数等。这些可调用对象虽然都可以通过 () 运算符调用,但它们的底层类型却存在本质差异:普通函数的类型由函数签名决定,仿函数的类型是自定义类类型,Lambda 表达式的类型是编译器生成的匿名类类型。这种类型差异导致了一个严重的问题:无法用统一的类型声明、存储、传递不同类型的可调用对象。
C++11 引入的 std::function 包装器,正是为了解决这一问题。std::function 是一种类型安全的通用可调用对象包装器,其核心优势在于可以包装任意符合特定签名的可调用对象,实现可调用对象的类型统一,极大地提升了代码的灵活性与可维护性。
2.1 std::function 的模板结构与核心特性
std::function 是一个模板类,定义在 <functional> 头文件中。其核心模板结构如下:
template <class T>
class function; // 未定义的基础模板
template <class Ret, class… Args>
class function<Ret(Args…)>; // 特化模板
其中,Ret 表示可调用对象的返回值类型,Args… 表示可调用对象的参数类型列表。Ret (Args…) 被称为函数签名,它决定了 std::function 可以包装的可调用对象的类型 —— 只有返回值类型与参数类型列表完全匹配的可调用对象,才能被该 std::function 包装。
std::function 的核心特性主要包括以下几个方面:
2.1.1 类型统一:打破可调用对象的类型壁垒
这是 std::function 最核心的特性。无论底层可调用对象的类型如何(普通函数、仿函数、Lambda 表达式等),只要其函数签名与 std::function 的模板参数匹配,就可以被 std::function 包装,并且包装后的对象具有统一的类型。
例如,一个普通函数 bool func1 (int a, int b)、一个仿函数 struct Func { bool operator ()(int a, int b) { return a > b; } }、一个 Lambda 表达式 [](int a, int b) { return a < b; },它们的函数签名都是 bool (int, int),因此都可以被 std::function<bool (int, int)> 包装,包装后的对象类型完全一致。
这种类型统一的特性,解决了不同类型可调用对象无法统一存储、传递的问题。例如,可以将多个不同类型的可调用对象存储在同一个 vector 容器中:vector<function<bool (int, int)>> vec {func1, Func (), [](int a, int b) { return a < b; }}。
代码示例:类型统一特性
#include <iostream>
#include <vector>
#include <functional>
using namespace std;
// 普通函数
bool compareGreater(int a, int b) {
return a > b;
}
// 仿函数
struct CompareLess {
bool operator()(int a, int b) {
return a < b;
}
};
int main() {
// 不同类型的可调用对象,但签名相同
auto lambdaEqual = [](int a, int b) { return a == b; };
// 用 std::function 统一类型
vector<function<bool(int, int)>> funcList;
funcList.push_back(compareGreater); // 普通函数
funcList.push_back(CompareLess()); // 仿函数
funcList.push_back(lambdaEqual); // Lambda 表达式
// 统一调用
int x = 5, y = 5;
for (size_t i = 0; i < funcList.size(); ++i) {
cout << "funcList[" << i << "](" << x << "," << y << ") = "
<< boolalpha << funcList[i](x, y) << endl;
}
return 0;
}
2.1.2 类型安全:编译期检查函数签名匹配性
与函数指针相比,std::function 具有更强的类型安全性。函数指针在进行隐式转换时可能会导致类型不匹配的问题,而 std::function 会在编译期严格检查可调用对象的函数签名是否与模板参数匹配。如果不匹配,编译器会直接触发编译错误,避免了运行时错误的发生。
例如,若尝试将一个签名为 int (int, int) 的函数包装到 std::function<bool (int, int)> 中,编译器会明确提示类型不匹配,从而帮助开发者在编译阶段发现问题。
代码示例:类型安全检查
#include <iostream>
#include <functional>
using namespace std;
// 签名:int(int, int)
int add(int a, int b) {
return a + b;
}
int main() {
// 正确:签名匹配
function<int(int, int)> f1 = add;
cout << "f1(3,4) = " << f1(3,4) << endl;
// 错误:签名不匹配(返回值类型不同),编译失败
// function<bool(int, int)> f2 = add;
return 0;
}
2.1.3 支持空状态:避免空指针调用风险
std::function 支持默认构造,默认构造的 std::function 对象处于空状态,即不持有任何可调用对象。可以通过 operator bool () 成员函数判断 std::function 对象是否为空,从而避免空指针调用的风险。
例如,在调用 std::function 对象之前,可以先进行空判断:if (f) { f (10, 20); }。这种方式比直接使用函数指针更加安全,因为函数指针的空指针调用会直接导致程序崩溃,而 std::function 的空状态判断可以有效避免这一问题。
代码示例:空状态检查
#include <iostream>
#include <functional>
using namespace std;
int multiply(int a, int b) {
return a * b;
}
int main() {
// 默认构造:空状态
function<int(int, int)> f;
// 空判断
if (!f) {
cout << "f 为空,无法调用" << endl;
}
// 赋值后变为非空
f = multiply;
if (f) {
cout << "f(5,6) = " << f(5,6) << endl; // 输出:30
}
// 清空
f = nullptr;
if (!f) {
cout << "f 已清空,无法调用" << endl;
}
return 0;
}
2.1.4 兼容所有可调用对象:尤其是有捕捉的 Lambda 表达式
函数指针的一个重要局限性是无法直接指向有捕捉的 Lambda 表达式。因为有捕捉的 Lambda 表达式对应的仿函数类包含成员变量,其大小不为零,而函数指针的大小是固定的(通常为 4 或 8 字节),无法存储仿函数类的成员变量。
std::function 则完美解决了这一问题,它可以包装任意类型的可调用对象,包括有捕捉的 Lambda 表达式。这是因为 std::function 内部采用了类型擦除技术,通过动态分配内存存储可调用对象的副本或引用,从而兼容不同大小、不同类型的可调用对象。
代码示例:包装有捕捉的 Lambda
#include <iostream>
#include <functional>
using namespace std;
int main() {
int base = 100;
// 有捕捉的 Lambda 表达式
auto lambda = [base](int a) {
return a + base;
};
// 函数指针无法指向有捕捉的 Lambda(编译错误)
// int (*fp)(int) = lambda;
// std::function 可以正常包装
function<int(int)> f = lambda;
cout << "f(50) = " << f(50) << endl; // 输出:150
return 0;
}
2.2 std::function 的基本使用方法
std::function 的使用方法较为简单,主要包括对象的创建、赋值、调用三个步骤,同时需要注意包装类成员函数时的特殊规则。
2.2.1 包装普通函数与仿函数
包装普通函数与仿函数的方式较为直接,只需要将可调用对象赋值给 std::function 对象即可。例如:
- 包装普通函数:function<bool (int, int)> f = func1;,其中 func1 是一个签名为 bool (int, int) 的普通函数。
- 包装仿函数:function<bool (int, int)> f = Func ();,其中 Func 是一个重载了 operator () 运算符的仿函数类。
赋值完成后,可以通过 () 运算符调用 std::function 对象,本质上是调用其包装的可调用对象。
代码示例:包装普通函数与仿函数
#include <iostream>
#include <functional>
using namespace std;
// 普通函数
double divide(double a, double b) {
if (b == 0) return 0;
return a / b;
}
// 仿函数
struct Multiply {
double operator()(double a, double b) {
return a * b;
}
};
int main() {
// 包装普通函数
function<double(double, double)> f1 = divide;
cout << "f1(10, 2) = " << f1(10, 2) << endl; // 输出:5
// 包装仿函数
function<double(double, double)> f2 = Multiply();
cout << "f2(10, 2) = " << f2(10, 2) << endl; // 输出:20
return 0;
}
2.2.2 包装 Lambda 表达式
包装 Lambda 表达式的方式与包装普通函数、仿函数类似,既可以直接将 Lambda 表达式赋值给 std::function 对象,也可以将 Lambda 表达式的变量赋值给 std::function 对象。例如:
- 直接赋值:function<bool (int, int)> f = [](int a, int b) { return a > b; };
- 通过变量赋值:auto lambda = [](int a, int b) { return a < b; }; function<bool (int, int)> f = lambda;
无论是有捕捉还是无捕捉的 Lambda 表达式,都可以被 std::function 包装,这是 std::function 相比函数指针的重要优势。
代码示例:包装 Lambda 表达式
#include <iostream>
#include <functional>
using namespace std;
int main() {
// 直接包装无捕捉 Lambda
function<int(int, int)> add = [](int a, int b) {
return a + b;
};
cout << "add(3,4) = " << add(3,4) << endl; // 输出:7
// 包装有捕捉 Lambda
int factor = 10;
auto multiply = [factor](int a, int b) {
return (a + b) * factor;
};
function<int(int, int)> f = multiply;
cout << "f(3,4) = " << f(3,4) << endl; // 输出:70
return 0;
}
2.2.3 包装类成员函数
包装类成员函数时,需要注意两个特殊规则:
具体来说,包装类成员函数分为两种情况:
- 包装静态成员函数:静态成员函数不包含 this 指针,其签名与普通函数一致。因此,包装静态成员函数的方式与包装普通函数相同,例如:function<int (int, int)> f = &Plus::plusi;,其中 plusi 是 Plus 类的静态成员函数,签名为 int (int, int)。
- 包装普通成员函数:普通成员函数包含 this 指针,因此 std::function 的模板参数需要将 this 指针的类型作为第一个参数。例如,Plus 类的普通成员函数 plusd 的签名为 double (double, double),其实际签名为 double (Plus*, double, double)(this 指针的类型为 Plus*)。因此,包装该函数的方式为:function<double (Plus*, double, double)> f = &Plus::plusd;。
调用包装普通成员函数的 std::function 对象时,需要传入对象的指针或引用作为第一个参数,例如:Plus p; f (&p, 1.1, 2.2);。除了传入指针,也可以传入对象本身或右值引用,此时 std::function 的模板参数需要相应调整为对象类型或右值引用类型。
代码示例:包装类成员函数
#include <iostream>
#include <functional>
using namespace std;
class Calculator {
public:
// 静态成员函数
static int addInt(int a, int b) {
return a + b;
}
// 普通成员函数
double addDouble(double a, double b) {
return a + b;
}
// 带成员变量的普通成员函数
double multiply(double a) {
return a * _factor;
}
Calculator(double f) : _factor(f) {}
private:
double _factor;
};
int main() {
// 1. 包装静态成员函数
function<int(int, int)> f1 = &Calculator::addInt;
cout << "f1(10,20) = " << f1(10,20) << endl; // 输出:30
// 2. 包装普通成员函数(传入对象指针)
function<double(Calculator*, double, double)> f2 = &Calculator::addDouble;
Calculator calc(2.0);
cout << "f2(&calc, 1.5, 2.5) = " << f2(&calc, 1.5, 2.5) << endl; // 输出:4.0
// 3. 包装带成员变量的普通成员函数
function<double(Calculator&, double)> f3 = &Calculator::multiply;
cout << "f3(calc, 5.0) = " << f3(calc, 5.0) << endl; // 输出:10.0
return 0;
}
2.3 std::function 的高级用法:类型别名与函数参数
在实际开发中,std::function 的一个常用高级用法是结合 using 关键字定义类型别名,从而简化代码书写。例如:using FuncType = function<bool (int, int)>;。定义类型别名后,可以直接使用该别名声明 std::function 对象,例如:FuncType f = [](int a, int b) { return a > b; };。
这种方式的优势在于,当需要修改函数签名时,只需要修改类型别名的定义,而不需要修改所有使用该类型的代码,极大地提升了代码的可维护性。
此外,std::function 还常被用作函数参数或返回值类型,用于接收任意符合签名的可调用对象。例如,一个函数可以接收一个 std::function 对象作为参数,实现对不同可调用对象的统一处理:
代码示例:类型别名与函数参数
#include <iostream>
#include <functional>
using namespace std;
// 定义类型别名简化代码
using CalcFunc = function<int(int, int)>;
// 接收 std::function 作为参数
void executeCalc(CalcFunc f, int a, int b, const string& desc) {
if (f) {
cout << desc << ": " << a << " 和 " << b << " 的计算结果:" << f(a, b) << endl;
} else {
cout << "函数对象为空!" << endl;
}
}
// 返回 std::function 对象
CalcFunc getAddFunc() {
return [](int a, int b) { return a + b; };
}
int main() {
// 使用类型别名声明对象
CalcFunc addFunc = [](int a, int b) { return a + b; };
CalcFunc subFunc = [](int a, int b) { return a – b; };
// 传递 std::function 作为参数
executeCalc(addFunc, 10, 5, "加法");
executeCalc(subFunc, 10, 5, "减法");
// 使用返回的 std::function 对象
CalcFunc func = getAddFunc();
cout << "getAddFunc()(20,30) = " << func(20,30) << endl;
return 0;
}
2.4 std::function 的底层实现原理:类型擦除技术
std::function 能够包装不同类型、不同大小的可调用对象,其底层依赖于类型擦除技术。类型擦除的核心思想是:将不同类型的可调用对象转化为统一的接口,通过多态机制实现对不同可调用对象的调用。
具体来说,std::function 的底层实现大致分为以下几个步骤:
通过这种方式,std::function 擦除了可调用对象的具体类型,只保留了其函数签名信息,从而实现了对不同可调用对象的包装。类型擦除技术是 std::function 实现类型统一的核心,也是其能够兼容所有可调用对象的根本原因。
代码示例:简易版类型擦除实现
#include <iostream>
#include <memory>
using namespace std;
// 1. 抽象基类:定义统一接口
template <typename Ret, typename… Args>
class FuncBase {
public:
virtual ~FuncBase() = default;
virtual Ret operator()(Args… args) = 0;
};
// 2. 模板派生类:包装具体的可调用对象
template <typename F, typename Ret, typename… Args>
class FuncDerived : public FuncBase<Ret, Args…> {
private:
F _func; // 存储可调用对象
public:
FuncDerived(F&& func) : _func(std::move(func)) {}
Ret operator()(Args… args) override {
return _func(std::forward<Args>(args)…);
}
};
// 3. 简易版 function 包装器
template <typename Signature>
class MyFunction;
template <typename Ret, typename… Args>
class MyFunction<Ret(Args…)> {
private:
unique_ptr<FuncBase<Ret, Args…>> _ptr; // 基类指针
public:
// 默认构造:空状态
MyFunction() = default;
// 赋值构造:接受任意可调用对象
template <typename F>
MyFunction(F&& func) {
_ptr = make_unique<FuncDerived<F, Ret, Args…>>(std::forward<F>(func));
}
// 调用运算符
Ret operator()(Args… args) const {
if (!_ptr) {
throw runtime_error("MyFunction is empty!");
}
return (*_ptr)(std::forward<Args>(args)…);
}
// 空判断
explicit operator bool() const {
return _ptr != nullptr;
}
};
// 测试
int main() {
// 包装普通函数
auto add = [](int a, int b) { return a + b; };
MyFunction<int(int, int)> f(add);
if (f) {
cout << "f(3,4) = " << f(3,4) << endl; // 输出:7
}
// 包装有捕捉的 Lambda
int base = 100;
auto addBase = [base](int a) { return a + base; };
MyFunction<int(int)> f2(addBase);
cout << "f2(50) = " << f2(50) << endl; // 输出:150
return 0;
}
三、std::bind 适配器:函数参数的灵活调整工具
在实际开发中,常常会遇到这样的场景:一个函数的参数个数过多,而我们只需要使用其中的一部分;或者一个函数的参数顺序不符合我们的需求,需要调整参数的传递顺序。在 C++11 之前,解决这些问题的唯一方法是重写函数的重载版本,这不仅增加了代码的冗余度,还降低了代码的可维护性。
C++11 引入的 std::bind 适配器,为解决这些问题提供了优雅的解决方案。std::bind 是一种函数适配器,其本质是一个函数模板,能够对现有的可调用对象进行参数层面的改造,生成一个新的可调用对象。新的可调用对象在调用时,会自动调用原可调用对象,并按照预设的规则传递参数。
3.1 std::bind 的核心概念与调用格式
std::bind 的核心作用是调整可调用对象的参数,其调用格式如下:
auto newCallable = bind(callable, arg_list);
其中,各个参数的含义如下:
- callable:待适配的原可调用对象,可以是普通函数、仿函数、Lambda 表达式、std::function 对象、类成员函数等。
- arg_list:逗号分隔的参数列表,用于指定原可调用对象的参数来源。该参数列表包含两种类型的元素:普通值和占位符。
- newCallable:std::bind 返回的新可调用对象,其类型是编译器生成的匿名类型,通常需要使用 auto 关键字接收。调用 newCallable 时,本质上是调用原可调用对象 callable,并按照 arg_list 的规则传递参数。
std::bind 的核心在于 arg_list 的设计,其中普通值与占位符的组合使用,决定了新可调用对象的参数个数与传递规则。
3.2 占位符:std::bind 的核心关键
占位符是 std::bind 实现参数调整的核心工具,定义在 std::placeholders 命名空间中。占位符的形式为 _n(n 是正整数),其核心含义如下:
- _1 表示调用新可调用对象时传入的第 1 个参数。
- _2 表示调用新可调用对象时传入的第 2 个参数。
- 以此类推,_n 表示调用新可调用对象时传入的第 n 个参数。
使用占位符时,需要注意以下两个规则:
占位符的本质是参数位置的标记,它标记了新可调用对象的参数需要传递给原可调用对象的哪个位置。通过调整占位符的顺序,可以灵活地改变参数的传递顺序;通过减少占位符的数量,可以固定部分参数,减少新可调用对象的参数个数。
3.3 std::bind 的三大核心功能
std::bind 的核心功能是调整可调用对象的参数,具体可以分为固定参数、调整参数顺序、混合调整三种场景,覆盖了绝大多数参数调整的需求。
3.3.1 功能一:固定参数 —— 减少新可调用对象的参数个数
这是 std::bind 最常用的功能。当原可调用对象的参数个数过多,而我们只需要使用其中的一部分时,可以通过 std::bind 将不需要的参数固定为具体值,从而减少新可调用对象的参数个数。
具体实现方式是:在 arg_list 中,将需要固定的参数替换为具体的普通值,需要动态传入的参数替换为占位符。例如,原函数 Sub 的签名为 int (int, int),表示计算两个数的差值乘以 10。如果我们需要固定第一个参数为 100,只保留第二个参数作为动态参数,可以这样使用 std::bind:auto sub3 = bind (Sub, 100, _1);。
新可调用对象 sub3 的参数个数为 1,调用 sub3 (5) 时,本质上是调用原函数 Sub (100, 5),计算结果为 (100 – 5) * 10 = 950。同理,如果固定第二个参数为 100,只保留第一个参数作为动态参数,可以写成 auto sub4 = bind (Sub, _1, 100);,调用 sub4 (5) 时,本质上是调用 Sub (5, 100),计算结果为 (5 – 100) * 10 = -950。
这种方式的优势在于,无需重写原函数的重载版本,就可以实现参数个数的减少,极大地提升了代码的灵活性。
代码示例:固定参数
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders; // 简化占位符使用
// 原函数:计算 (a – b) * 10
int Sub(int a, int b) {
return (a – b) * 10;
}
int main() {
// 固定第一个参数为 100,只保留第二个参数
auto sub3 = bind(Sub, 100, _1);
cout << "sub3(5) = " << sub3(5) << endl; // 输出:950 (100-5)*10
// 固定第二个参数为 100,只保留第一个参数
auto sub4 = bind(Sub, _1, 100);
cout << "sub4(5) = " << sub4(5) << endl; // 输出:-950 (5-100)*10
// 固定所有参数:新函数无参数
auto sub5 = bind(Sub, 20, 10);
cout << "sub5() = " << sub5() << endl; // 输出:100 (20-10)*10
return 0;
}
3.3.2 功能二:调整参数顺序 —— 改变参数的传递顺序
在某些场景下,原可调用对象的参数顺序不符合我们的需求,需要调整参数的传递顺序。使用 std::bind 可以通过调整占位符的顺序,轻松实现这一功能,无需重写函数。
具体实现方式是:在 arg_list 中,调整占位符的顺序,使其与原可调用对象的参数顺序不一致。例如,原函数 Sub 的签名为 int (int, int),计算逻辑为 (a – b) * 10。如果我们需要调整参数顺序,计算 (b – a) * 10,可以这样使用 std::bind:auto sub2 = bind (Sub, _2, _1);。
新可调用对象 sub2 的参数个数仍为 2,但参数顺序发生了变化。调用 sub2 (10, 5) 时,占位符 _2 对应传入的第 2 个参数 5,占位符 _1 对应传入的第 1 个参数 10,因此本质上是调用原函数 Sub (5, 10),计算结果为 (5 – 10) * 10 = -50。
这种方式通过调整占位符的顺序,实现了参数传递顺序的改变,极大地简化了代码的修改成本。
代码示例:调整参数顺序
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
// 原函数:比较 a 是否大于 b
bool isGreater(int a, int b) {
cout << "比较 " << a << " > " << b << " ? ";
return a > b;
}
int main() {
// 原函数调用
cout << boolalpha << isGreater(10, 5) << endl; // 输出:true
// 调整参数顺序:比较 b 是否大于 a
auto isLess = bind(isGreater, _2, _1);
cout << boolalpha << isLess(10, 5) << endl; // 输出:false
// 三元参数调整顺序示例
auto func = [](int a, int b, int c) {
return a * 100 + b * 10 + c;
};
// 调整为 c, a, b 的顺序
auto func2 = bind(func, _2, _3, _1);
cout << "func2(1,2,3) = " << func2(1,2,3) << endl; // 输出:231 (2*100 +3*10 +1)
return 0;
}
3.3.3 功能三:混合调整 —— 固定参数与调整顺序的结合
在实际开发中,常常需要同时实现固定参数与调整参数顺序,这就是 std::bind 的混合调整功能。具体实现方式是:在 arg_list 中,同时使用普通值(固定参数)和占位符(调整顺序),从而实现复杂的参数调整需求。
例如,原函数 SubX 的签名为 int (int, int, int),计算逻辑为 (a – b – c) * 10。如果我们需要固定第一个参数为 100,同时调整后两个参数的顺序,可以这样使用 std::bind:auto sub5 = bind (SubX, 100, _2, _1);。
新可调用对象 sub5 的参数个数为 2,调用 sub5 (5, 1) 时,第一个参数 100 是固定值,占位符 _2 对应传入的第 2 个参数 1,占位符 _1 对应传入的第 1 个参数 5,因此本质上是调用原函数 SubX (100, 1, 5),计算结果为 (100 – 1 – 5) * 10 = 940。
混合调整功能结合了固定参数与调整顺序的优势,能够满足绝大多数复杂的参数调整需求,是 std::bind 灵活性的集中体现。
代码示例:混合调整参数
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
// 原函数:计算 (a – b – c) * 10
int SubX(int a, int b, int c) {
return (a – b – c) * 10;
}
// 计算复利:本金 * (1 + 利率)^年数 – 本金
double compoundInterest(double rate, double principal, int years) {
double result = principal;
for (int i = 0; i < years; ++i) {
result *= (1 + rate);
}
return result – principal;
}
int main() {
// 1. 固定第一个参数,调整后两个参数顺序
auto sub5 = bind(SubX, 100, _2, _1);
cout << "sub5(5,1) = " << sub5(5,1) << endl; // 输出:940 (100-1-5)*10
// 2. 固定利率和年数,只保留本金作为参数
auto calcInterest = bind(compoundInterest, 0.03, _1, 5);
cout << "10000 元 5 年复利(利率 3%):" << calcInterest(10000) << endl;
// 3. 固定年数,调整利率和本金顺序
auto calcInterest2 = bind(compoundInterest, _2, _1, 10);
cout << "20000 元 10 年复利(利率 4%):" << calcInterest2(20000, 0.04) << endl;
return 0;
}
3.4 std::bind 与 std::function 的结合使用
std::bind 返回的新可调用对象是编译器生成的匿名类型,无法直接使用类型名声明,只能通过 auto 关键字接收。这种匿名类型在作为函数参数或容器元素时会受到限制,而 std::function 可以完美解决这一问题。
std::function 可以包装 std::bind 返回的新可调用对象,实现类型的统一。具体实现方式是:根据新可调用对象的函数签名,定义对应的 std::function 类型,然后将 std::bind 返回的对象赋值给 std::function 对象。
例如,原 Lambda 表达式 func1 的签名为 double (double, double, int),表示计算复利的利息。通过 std::bind 固定利率为 0.015、年数为 3,得到新可调用对象 func3_1_5,其签名为 double (double)(仅接收本金作为参数)。使用 std::function 包装该对象的方式为:function<double (double)> f = bind (func1, 0.015, _1, 3);。
这种结合使用的方式,既发挥了 std::bind 灵活调整参数的优势,又利用了 std::function 统一类型的特性,极大地提升了代码的灵活性与可维护性。
代码示例:bind 与 function 结合使用
#include <iostream>
#include <functional>
#include <vector>
using namespace std;
using namespace placeholders;
// 计算商品折扣价:原价 * (1 – 折扣率)
double discountPrice(double price, double discount) {
return price * (1 – discount);
}
int main() {
// 1. 绑定固定折扣率,生成新的可调用对象
auto discount8 = bind(discountPrice, _1, 0.2); // 8 折
auto discount9 = bind(discountPrice, _1, 0.1); // 9 折
// 2. 用 std::function 统一类型
using DiscountFunc = function<double(double)>;
vector<DiscountFunc> discountFuncs;
discountFuncs.push_back(discount8);
discountFuncs.push_back(discount9);
discountFuncs.push_back(bind(discountPrice, _1, 0.05)); // 95 折
// 3. 统一调用
double originalPrice = 100.0;
for (size_t i = 0; i < discountFuncs.size(); ++i) {
cout << "折扣 " << (i==0?"8折":(i==1?"9折":"95折"))
<< " 价格:" << discountFuncs[i](originalPrice) << endl;
}
return 0;
}
3.5 std::bind 的特殊用法:包装类成员函数
std::bind 包装类成员函数的方式与 std::function 类似,需要注意两个特殊规则:
例如,包装 Plus 类的普通成员函数 plusd,可以这样使用 std::bind:function<double (double, double)> f = bind (&Plus::plusd, Plus (), _1, _2);。其中,Plus () 是一个临时对象,用于初始化 this 指针。调用 f (1.1, 2.2) 时,本质上是调用临时对象的 plusd 函数,计算结果为 1.1 + 2.2 = 3.3。
除了传入临时对象,还可以传入对象的指针或引用,例如 bind (&Plus::plusd, &p, _1, _2)(p 是 Plus 类的对象),此时 this 指针指向对象 p。
代码示例:包装类成员函数
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
class MathUtil {
public:
// 普通成员函数:乘法
double multiply(double a, double b) {
return a * b * _factor;
}
// 静态成员函数:除法
static double divide(double a, double b) {
if (b == 0) return 0;
return a / b;
}
MathUtil(double f) : _factor(f) {}
private:
double _factor;
};
int main() {
// 1. 包装普通成员函数:绑定对象实例
MathUtil util(2.0);
auto mul = bind(&MathUtil::multiply, &util, _1, _2);
cout << "mul(3,4) = " << mul(3,4) << endl; // 输出:24 (3*4*2)
// 2. 包装普通成员函数:固定一个参数
auto mulBy10 = bind(&MathUtil::multiply, util, _1, 10);
cout << "mulBy10(5) = " << mulBy10(5) << endl; // 输出:100 (5*10*2)
// 3. 包装静态成员函数:无需绑定对象
auto div = bind(&MathUtil::divide, _1, _2);
cout << "div(10,2) = " << div(10,2) << endl; // 输出:5
// 4. 结合 std::function 使用
function<double(double)> divBy2 = bind(&MathUtil::divide, _1, 2);
cout << "divBy2(20) = " << divBy2(20) << endl; // 输出:10
return 0;
}
3.6 std::bind 的使用注意事项
std::bind 虽然灵活,但在使用时需要注意以下几个问题,避免出现不必要的错误:
代码示例:bind 使用注意事项
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
// 引用参数的函数
void modifyValue(int& val, int add) {
val += add;
}
int main() {
// 1. 引用参数传递:必须使用 std::ref
int x = 10;
// 错误方式:绑定的是拷贝,不会修改原变量
auto badBind = bind(modifyValue, x, _1);
badBind(5);
cout << "badBind 后 x = " << x << endl; // 输出:10
// 正确方式:使用 std::ref 传递引用
auto goodBind = bind(modifyValue, ref(x), _1);
goodBind(5);
cout << "goodBind 后 x = " << x << endl; // 输出:15
// 2. const 引用:使用 std::cref
const int y = 100;
auto print = [](const int& v) { cout << "值:" << v << endl; };
auto printY = bind(print, cref(y));
printY(); // 输出:100
// 3. Lambda 替代简单 bind 场景
int base = 10;
// bind 方式
auto addBind = bind([](int a, int b) { return a + b; }, _1, base);
// Lambda 方式(更简洁)
auto addLambda = [base](int a) { return a + base; };
cout << "addBind(5) = " << addBind(5) << endl; // 输出:15
cout << "addLambda(5) = " << addLambda(5) << endl; // 输出:15
return 0;
}
最佳实践:工具选择的优先级
在实际开发中,选择 Lambda 表达式、std::function 或 std::bind 时,应遵循以下优先级原则:
五、总结
Lambda 表达式、std::function 包装器与 std::bind 适配器是 C++11 引入的三大核心工具,它们分别从轻量级函数定义、可调用对象类型统一、函数参数适配三个维度,为开发者提供了简洁、灵活、高效的可调用对象处理方案。
Lambda 表达式作为匿名函数对象的语法糖,支持捕捉外层变量,极大地简化了轻量级函数逻辑的实现;std::function 作为通用可调用对象包装器,打破了不同类型可调用对象的类型壁垒,实现了类型的统一存储与传递;std::bind 作为函数适配器,能够灵活地调整可调用对象的参数,实现参数的固定与顺序调整。
在实际开发中,合理地结合使用这三大工具,遵循 “优先使用 Lambda 表达式,类型统一使用 std::function,复杂参数调整使用 std::bind” 的原则,可以显著提升代码的简洁性、可读性与可维护性,构建更加优雅的现代 C++ 程序。
完整示例代码:
#include <iostream>
#include <functional>
#include <algorithm>
using namespace std;
// OK,那么在本文件中,我们来学习一些新东西:lambda表达式,包装器function,包装器bind
// 那么其中最核心的就是lambda表达式,这个是很轻量化的一个函数表达形式
// 其实我们之前在Linux线程部分,也有用到过,那么在这里,我们就来详细的解析一下
namespace lambda_funtion_bind
{
// 那么lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
// lambda 表达式语法使用层而言没有类型,
// 所以我们一般是用auto或者模板参数定义的对象去接收lambda 对象。
// 所以我们就知道了,哦,lambda本质上是一个匿名函数对象啊
// 我们可以使用auto去接收该对象,那么此时该变量就是相当于为一个函数
// 可以使用变量(参数)而调用lambda所形成的函数
// 这一点大家要是要知道的
// 接下来我们来了解一下lambda的格式:
// [] () -> 返回值的类型 {}
// 就是长这个样子,而要是我们使用变量接收的话:
// auto func=[] () -> 返回值的类型 {}
// 调用该函数就是 func()
// lambda表达式的格式: [capture-list] (parameters)-> return type {function boby }
// 那么格式就是那样子了,接下来我们来说=详细解析各个括号是什么以及要放什么
// 我们先说和我们平时函数有关联的
// 我们先写个普通函数
int add(int a, int b)
{
return a + b;
}
// 第一个是 () ,我们就是要往这里面放形参,就是类似正常函数的(),里面要写形参类型+形参名字
// 就是相当于上面普通函数的 (int a,int b) 部分,而要是该函数用不到参数的话
// 也可以把()省略掉
// 这个还是很简单的,大家肯定随手拿捏
// 第二个就是 -> 返回值的类型 那么这个就更简单了,这个就是要在->后面加上你所写的这个函数的
// 返回值的类型,就是相当于正常函数中的最前面的返回值类型
// 就是相当于上面普通函数的 int 部分
// 也是很简单,那么有所不同的就是说,在lambda表达式中,我们是可以省略 -> 返回值类型 的
// 即lambda表达式可以简化为 [] () {} ,是的,就是这么牛波一
// 之所以能这样的原因是在于编译器可以自动推导出函数返回值类型的,所以也就不需要我们显式指定
// 但是我们要注意,就是说要是函数体内部返回是使用if else语句返回不同类型的话
// 那么我们就得显式指定返回值类型!!!
// 第三个就是 {} ,这个也是很简单,我们就是要往这里面放函数主体,简称函数体
// 那么就是我们平时正常函数中往{}里面放什么,我们就怎么往lambda中的{}怎么放
// 他就是相当于为函数的主体,也就是函数的主要运行逻辑!!!
// 就是相当于上面普通函数的
// {
// return a+b;
// }
// 部分
// 那么前面说了lambda表达式的本质是匿名函数对象,所以在正常的lambda表达式中自然没有放函数名字的地方
// 当然,你要是使用相对应的变量接收该lambda表达式的话
// 那么该变量名字就相当于是函数名字,即相当于上面普通函数的add
// • (parameters) :参数列表,与普通函数的参数列表功能类似,
// 如果不需要参数传递,则可以连同()一起省略
// • ->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。
// 一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
// • {function boby} :函数体,函数体内的实现跟普通函数完全类似,
// 在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
// 接下来的就是[]部分,这个的名字叫做捕捉列表
// 这个算是lambda表达式的核心部分了
// 那么我们想想看,函数体内部是不是可能会用到函数前面的变量或者数据呢???
// 那么如果我们只有形参(即传入的参数)的话,函数体内部是肯定无法用到前面的数据的
// 虽然说,我们的普通函数是无法直接获取到非传参的非全局or静态的变量,但是都是叫做普通函数了,所以肯定low一下
// 那么lambda表达式作为轻量级可直接在其他函数内部写的匿名函数对象,它肯定要高级一点
// 所以它就得可以获取到在它前面的非传参的非全局or静态的变量,那么在他后面肯定无法获取
// 毕竟编译器编译是从上到下的!!!
// 那么lambda表达式是依靠什么去获取到它前面的非传参的非全局or静态的变量呢??
// 答案就是[]——捕捉列表!!!
// 那么我们就可以通过它去获取到lambda表达式前面的非传参的非全局or静态的变量
// 那么具体是怎么使用捕捉列表的呢???
// 有挺多种方式,我们一种一种来说
// 第一种就是直接把lambda表达式前面的非传参的非全局or静态的变量名字写入
// 比如这样子:
int main()
{
int a = 10;
double d = 1.1;
char c = 'a';
int t = 0;
// lambda表达式
auto func = [a, d, c](int num) -> int
{int sum=a+d+c+num; return sum; };
int ret = func(t);
// 可以看到,这里我们就是在[]捕捉列表中将lambda表达式前面的非传参的非全局or静态的变量名字写入
// 那么在lambda函数体中,我们就可以使用到这些变量!!!
// 但是呢,要注意,这里的捕捉,只是传值捕捉
// 意思就是说,对于直接写变量名字的捕捉而言
// 它们的前面都有个const修饰,这也就意味着说,对于这些变量
// 我们只能在函数体内使用它们的值,不能去修改它们的值,即不能修改这些变量!!!
// lambda表达式
auto func1 = [a, d, c](int num) -> int
{
//++a;//报错,表达式必须是可修改的左值
int sum = a + d + c + num;
return sum;
};
int ret1 = func1(t);
// 那么你要是说我就是想修改捕捉到的变量值,诶,那么有两种方式供你选择
// 第一种就是在)后面加mutable,表示为可变的,相当于是在函数声明后面添加mutable
// 那么此时你就能对捕捉到的变量的值进行修改,但是注意,这里只是修改了函数体内可使用的值
// 而对于外面的被捕捉到的变量而言,它们的值是没有被修改的
// 总结一下这个就是相当于是传值传参!!!
// lambda表达式
auto func2 = [a, d, c](int num) mutable -> int
{
++a; // 不报错,不会修改掉外面的a的值
int sum = a + d + c + num;
return sum;
};
int ret2 = func2(t);
// 那么上面说的添加mutable的方式是可以修改捕捉到的变量值
// 但是不会影响到外面的被捕捉到的变量的值
// 那么另一种方式就是可以影响到外面的被捕捉到的变量的值
// 也就是在要捕捉的变量名字前面加&
// 这个就是相当于引用传参!!!
// 就是相当于我们平时函数形参类型为 类型& 形参名字
// 可以通过在函数体内部修改形参而影响到被传递的实参
// 在lambda中,当我们在[]捕捉列表中要捕捉的变量名字前面加&的话
// 就是相当于这样子的功效
// lambda表达式
auto func3 = [&a, &d, &c](int num) -> int
{
++a; // 会修改掉外面的a的值
int sum = a + d + c + num;
return sum;
};
int ret3 = func3(t);
cout << a << endl;
// 那么,要是我们有些变量想修改值,有些变量不想的话,应该怎么办呢??
// 其实很简单,就是既用直接变量名字,又用&变量名字即可,这个还是很简单的
// lambda表达式
auto func4 = [&a, d, c](int num) -> int
{
++a; // 会修改掉外面的a的值
int sum = a + d + c + num;
return sum;
};
int ret4 = func4(t);
cout << a << endl;
// 那么还有就是,要是我们不想自己捕捉,我就是懒,那么要怎么办呢???
// 那么就是捕捉列表中隐式捕捉,
// 们在捕捉列表写一个=表示隐式值捕捉,
// 在捕捉列表写一个&表示隐式引用捕捉,
// 这样我们lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。
// 编译器可不会蠢得把前面的所有变量全部捕捉!!!
// 但是要注意的是:可不能同时写=和&,这是不被允许的,毕竟这也太为难编译器了
// 那么同样的道理,我们也可以写一个& 然后对于不想改变值的变量,就再单独写出来
// =也是,对于想改变值的变量,我们再单独写出来即可,下面是示例
auto func5 = [&](int num) -> int
{
++a; // 会修改掉外面的a的值
int sum = a + d + c + num;
return sum;
};
int ret5 = func5(t);
auto func6 = [=](int num) -> int
{
//++a;//报错
int sum = a + d + c + num;
return sum;
};
int ret6 = func6(t);
auto func7 = [&, d](int num) -> int
{
++a; // 会修改掉外面的a的值
//++d;//报错
int sum = a + d + c + num;
return sum;
};
int ret7 = func7(t);
auto func8 = [=, &d](int num) -> int
{
//++a;//报错
++d; // 不报错
int sum = a + d + c + num;
return sum;
};
int ret8 = func8(t);
return 0;
}
// 那么可能就有人好奇了,
// lambda有什么用呢???作用在哪里呢???
// 那么肯定是很多的
// 除了线程之外
// 我们还可以直接使用它去替代仿函数传入类模版中进行比较大小
// 看看下面这个示例:
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
// …
Goods(const char *str, double price, int evaluate)
: _name(str), _price(price), _evaluate(evaluate)
{
}
};
struct ComparePriceLess
{
bool operator()(const Goods &gl, const Goods &gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods &gl, const Goods &gr)
{
return gl._price > gr._price;
}
};
int main()
{
vector<Goods> v = {{"苹果", 2.1, 5}, {"香蕉", 3, 4}, {"橙子", 2.2, 3}, {"菠萝", 1.5, 4}};
// 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中
// 不同项的比较,相对还是比较麻烦的,那么这里lambda就很好用了
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
{ return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
{ return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
{ return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
{ return g1._evaluate > g2._evaluate; });
return 0;
}
// 可以看到,我们使用lambda的话,就会方便很多,也直观很多!!!
// 那么这里再简单提一嘴,lambda的原理就是形成仿函数哦
/*
一、Lambda 核心本质
Lambda 表达式是 C++11 引入的匿名函数对象,核心特点的是突破普通函数的限制 ——
可直接定义在函数内部,无需像普通函数那样只能在全局或命名空间中声明定义;
语法层面没有显式类型,必须通过auto、函数模板或std::function接收;
底层本质是编译器自动生成的仿函数类(后续原理部分会详细拆解),
最大优势是支持捕捉外层作用域的变量,这是普通函数无法直接实现的功能。
二、Lambda 完整语法格式(逐部分拆解)
Lambda 的标准语法格式为:[capture-list] (parameters) -> return-type { function-body },
每个部分的作用、规则和省略条件都有明确要求,缺一不可(符合省略规则的除外)。
(一)[capture-list] 捕捉列表(核心重点,最易出错)
捕捉列表是 Lambda 的起始标识,也是最核心的部分,
作用是捕捉 Lambda 定义位置的外层作用域变量,供函数体内部使用,核心规则和捕捉方式如下:
1. 基础规则
必须作为 Lambda 的开头,哪怕不需要捕捉任何变量,[]也不能省略
(这是编译器识别 Lambda 的关键标识);
只能捕捉 “Lambda 定义位置之前” 的局部变量,无法捕捉定义之后声明的局部变量;
静态局部变量和全局变量不需要捕捉,可直接在 Lambda 函数体中使用
(因为它们的作用域是全局可见的,不受局部作用域限制);
如果 Lambda 定义在全局位置(而非函数内部),捕捉列表必须为空 ——
因为全局作用域没有 “外层局部变量” 可捕捉。
2. 三种捕捉方式(覆盖所有使用场景)
(1)显式捕捉:精准控制单个变量的捕捉方式
显式捕捉是指在捕捉列表中明确指定每个变量的捕捉类型,多个变量用逗号分隔,
适用于需要精准控制变量访问方式的场景。
语法格式:[变量1, &变量2, 变量3],其中不加&表示 “值捕捉”(拷贝外层变量的副本到 Lambda 内部),加&表示 “引用捕捉”(相当于给外层变量起别名,直接操作原变量);
示例:外层有变量a=10, b=20, c=30,定义auto func = [a, &b](),
则a是值捕捉(Lambda 内部用的是a的副本),b是引用捕捉(Lambda 内部操作的是原变量b),
c未被捕捉,若在函数体中使用c会直接编译报错;
注意:值捕捉的变量默认是只读的,无法在 Lambda 内部修改(除非加mutable修饰符)。
(2)隐式捕捉:编译器自动捕捉所需变量
隐式捕捉不需要手动指定变量名,只需要用=或&指定默认捕捉规则,
编译器会自动捕捉函数体中用到的所有外层局部变量,适用于需要使用多个变量、且捕捉方式一致的场景。
语法格式 1:[=]—— 隐式值捕捉,函数体中用到的所有外层局部变量,都以值捕捉的方式获取副本;
示例:外层有x=100, y=200, z=300,auto func = [=](),若函数体中使用了x和y,
编译器会自动以值捕捉的方式拷贝x和y的副本,z未被使用则不捕捉;
语法格式 2:[&]—— 隐式引用捕捉,函数体中用到的所有外层局部变量,都以引用捕捉的方式关联原变量;
示例:auto func = [&](),函数体中修改x的值,会直接影响外层原变量x;
注意:隐式值捕捉的变量同样默认只读,隐式引用捕捉的变量可直接修改原变量。
(3)混合捕捉:隐式 + 显式结合(有严格规则)
混合捕捉是隐式捕捉和显式捕捉的结合,适用于大部分变量采用同一捕捉方式,
个别变量需要特殊处理的场景,核心规则如下:
规则 1:捕捉列表的第一个元素必须是=或&(先指定默认捕捉方式);
规则 2:若默认捕捉方式是=(隐式值捕捉),后续显式指定的变量必须是引用捕捉(即变量前加&),
不能再是值捕捉(否则编译报错);
规则 3:若默认捕捉方式是&(隐式引用捕捉),后续显式指定的变量必须是值捕捉(即变量前不加&),
不能再是引用捕捉;
示例 1:[=, &x]—— 默认值捕捉所有用到的变量,唯独x采用引用捕捉;
示例 2:[&, y, z]—— 默认引用捕捉所有用到的变量,唯独y和z采用值捕捉。
(二)(parameters) 参数列表(和普通函数一致)
参数列表的作用是接收外部传入的参数,功能和普通函数的参数列表完全相同,核心规则和省略条件如下:
语法格式和普通函数一致:(参数类型1 参数名1, 参数类型2 参数名2, …),
支持默认参数、可变参数等特性;
省略规则:如果 Lambda 不需要接收外部参数,参数列表可以连同()一起省略;
但如果使用了mutable修饰符,即使没有参数,()也不能省略(这是mutable的强制要求);
示例 1:有参数 ——auto add = [](int x, int y) { return x + y; },
接收两个int类型参数,返回它们的和;
示例 2:无参数(可省略())——auto printHello = [] { cout << "Hello Lambda" << endl; }。
(三)-> return-type 返回值类型(可推导,常省略)
返回值类型用于指定 Lambda 的返回值类型,采用 “追踪返回类型” 语法,核心规则和省略条件如下:
省略规则 1:
如果 Lambda 没有返回值(函数体中无return语句,或return语句无返回值),
-> return-type可直接省略(默认返回void);
省略规则 2:
如果 Lambda 有返回值,且所有return语句的返回类型一致,
编译器会自动推导返回值类型,-> return-type也可省略;
必须显式指定的场景:
如果 Lambda 有多个return语句,
且返回类型不同(如一个返回int,一个返回double),必须显式指定返回值类型,
否则编译器无法推导,会报错;
示例:auto calc = [](int x, bool flag) -> double
{ if (flag) return x * 2; else return x + 0.5; },
因返回值可能是int或double,需显式指定返回double。
(四){function-body} 函数体(核心逻辑实现)
函数体是 Lambda 的核心逻辑所在,和普通函数的函数体完全一致,核心规则如下:
可使用的变量:参数列表中的参数 + 捕捉列表中捕捉的变量 + 全局 / 静态变量;
语法要求:即使函数体为空,{}也不能省略;
示例:auto func = [a, &b]() { cout << "a的副本:" << a << endl; b = 200; },
函数体中打印值捕捉的a,并修改引用捕捉的b。
三、关键修饰符:mutable(解除值捕捉的 const 限制)
mutable是 Lambda 的可选修饰符,核心作用是取消值捕捉变量的const属性,
允许在函数体中修改值捕捉的变量,核心规则如下:
作用范围:仅对 “值捕捉的变量” 有效,对引用捕捉的变量无影响(引用捕捉本身就可修改原变量);
关键注意:修改的是 “值捕捉的副本”,不会影响外层原变量 —— 因为值捕捉本质是拷贝,
mutable只是允许修改副本,原变量依然保持不变;
强制要求:使用mutable后,参数列表()不可省略,即使 Lambda 没有参数;
示例:外层int num = 5,auto func = [num]() mutable { num = 50; cout << "副本num:" << num << endl; },调用func()后,副本num变为 50,但外层原变量num仍为 5。
四、Lambda 的核心应用场景(对比传统可调用对象)
在 Lambda 出现之前,C++ 的可调用对象主要是 “函数指针” 和 “仿函数”,
但两者都有明显痛点:函数指针类型定义复杂,且无法捕捉外层变量;
仿函数需要自定义类,代码冗余。
Lambda 完美解决了这些问题,成为轻量级可调用对象的首选,典型应用场景如下:
(一)STL 算法自定义逻辑(如 sort 排序)
STL 算法(如sort、find_if)常需要传入自定义逻辑(如排序规则、筛选条件),Lambda 相比仿函数更简洁,无需定义额外类。
示例:对vector<int> vec = {3,1,4,2,5}排序,用仿函数需要定义一个类并重载operator(),而用 Lambda 可直接在sort参数中写逻辑:sort(vec.begin(), vec.end(), [](int a, int b) { return a < b; }),一行代码实现升序排序。
(二)线程函数(自定义线程执行逻辑)
创建线程时,Lambda 可直接捕捉外层变量,自定义线程的执行逻辑,无需定义全局函数或类成员函数。
示例:int threadId = 1; int count = 3;,创建线程thread t([threadId, count]() { for (int i=0; i<count; ++i) { cout << "线程" << threadId << "执行第" << i+1 << "次" << endl; } });,线程函数直接使用捕捉的threadId和count,逻辑清晰且无需额外定义。
(三)智能指针自定义删除器
智能指针(如unique_ptr、shared_ptr)默认使用delete释放资源,
但对于动态数组、文件指针等特殊资源,需要自定义删除器,Lambda 可简洁实现这一逻辑。
示例:创建动态数组的智能指针,自定义删除器释放数组:
int* arr = new int[5]{1,2,3,4,5};
unique_ptr<int, function<void(int*)>> up(arr, [](int* p)
{ delete[] p; cout << "动态数组已释放" << endl; });;
另一示例:文件指针的删除器:FILE* fp = fopen("test.txt", "w");
unique_ptr<FILE, function<void(FILE*)>> fpUp(fp, [](FILE* p)
{ fclose(p); cout << "文件已关闭" << endl; });。
五、Lambda 的底层原理(编译器视角)
Lambda 的底层本质是编译器自动生成的匿名仿函数类,我们写的 Lambda 代码,会被编译器转化为一个完整的仿函数类,两者完全等价,具体对应关系如下:
1. 核心等价关系
Lambda 代码片段编译器生成的仿函数类逻辑
auto add = [a](int x) { return a + x; }1. 生成一个名称唯一的仿函数类(如__lambda_123);
2. 类中定义私有成员变量int a(对应 Lambda 的捕捉列表变量);
3. 类的构造函数接收int a_,初始化成员变量a(a_);
4. 重载operator():int operator()(int x) const { return a + x; }(对应 Lambda 的参数、返回值和函数体);
5. 实例化仿函数对象:__lambda_123 add(a);
2. 关键细节解读
仿函数类名由编译器按规则生成(如包含行号、随机标识),确保不同 Lambda 生成的类名不冲突;
捕捉列表的变量会转化为仿函数类的成员变量:值捕捉对应普通成员变量(拷贝初始化),
引用捕捉对应引用类型成员变量(关联原变量);
Lambda 的参数、返回值、函数体,分别对应仿函数operator()的参数、返回值、函数体;
Lambda 值捕捉默认只读的原因:仿函数的operator()默认是const成员函数,
const成员函数无法修改类的普通成员变量;
而mutable修饰符会去掉operator()的const属性,允许修改成员变量(即值捕捉的副本)。
六、核心总结
本质:Lambda 是匿名函数对象,底层是编译器生成的仿函数类,可在函数内部定义,支持捕捉外层变量;
语法核心:捕捉列表[]是标识,参数列表可省略(无参数时),返回值可推导(类型一致时),
函数体{}不可省略;
捕捉规则:显式捕捉精准,隐式捕捉简洁,混合捕捉需遵循 “默认在前、类型匹配” 规则;
值捕捉只读(需mutable解除),引用捕捉可修改原变量;
优势:相比函数指针更灵活(可捕捉变量),相比仿函数更简洁(无需定义类);
应用:STL 算法、线程函数、智能指针删除器等场景,是 C++11 + 后轻量级可调用对象的首选。
*/
// OK,接下来我们来学习一下function包装器
// 其实大家对它肯定是完全不陌生!!!
// 因为我们之前就在Linux部分中经常用它包装函数,然后外界再传入函数
// 比如线程池中的任务调度,我们就是使用function去包装函数类型
// 然后在外界就可以直接把对应的函数传入,然后用function包装后的变量去接收
// 那么function的一个最核心的作用就是它可以把任意的函数啊,lambda等等都包装成一个好用浅显的类型
// 这样在很多地方就方便声明可调用对象的类
// template <class T>
// class function; // undefined
// template <class Ret, class… Args>
// class function<Ret(Args…)>;
// 函数指针、仿函数、lambda 等均属于 C++ 中的可调用对象,但它们的底层类型存在本质差异:
// 函数指针是指向函数的指针类型(如int (*)(int, int)),
// 类型由函数签名(返回值 + 参数)和函数地址唯一确定;
// 仿函数是自定义类重载operator()后的类类型
// (如struct Add { int operator()(int, int) { … } };),不同仿函数对应不同的类类型;
// lambda 表达式的底层是编译器生成的匿名闭包类型(无显式类型名),
// 即使签名相同的两个 lambda,其类型也被编译器视为不同;
// 这种类型碎片化会导致严重的开发痛点:
// 无法用统一的类型声明、存储、传递这些可调用对象
// (比如容器无法同时存储函数指针和 lambda,函数参数无法兼容不同类型的可调用对象)。
// std::function 是 C++11 引入的类型安全的通用函数包装器(位于<functional>头文件),
// 其核心优势正是打破可调用对象的类型壁垒,统一所有符合相同签名的可调用对象类型:
// 类型统一:只要可调用对象的签名(返回值类型 + 参数列表类型)一致,
// 无论其底层是函数指针、仿函数还是 lambda(包括有捕捉的 lambda),
// 都能被同一类型的std::function包装;
// 类型安全:相比函数指针的隐式转换风险,
// std::function在编译期检查可调用对象的签名是否匹配,不匹配则直接报错;
// 灵活存储与传递:可作为函数参数、返回值、容器元素
// (如vector<std::function<int(int, int)>>),
// 解决了不同类型可调用对象无法统一存储 / 传递的问题;
// 支持空状态:可默认构造(空状态),
// 也可通过operator bool()判断是否持有有效的可调用对象,避免空指针调用风险;
// 兼容所有可调用对象:
// 尤其是能包装有捕捉的 lambda(函数指针无法直接接收有捕捉的 lambda),弥补了函数指针的局限性。
// 大家可以发现,function的使用,是真的很爽!!
// 它可以把各种不同的,零碎的类型都包装起来
// 下面我们就来一些简单的示例
// 仿函数
struct func
{
bool operator()(int a, int b)
{
return a > b;
}
};
// 普通函数
bool func1(int a, int b)
{
return a > b;
}
int main()
{
// lambda表达式
auto la = [](int a, int b)
{ return a > b; };
// 我们知道,上面的三个的本质都是函数,我们都想把它们当作函数来使用
// 但是由于它们的类型都不同,所以我们也不能把它们存储进同一个容器变量中
// 但是有了function之后,这个就是小问题了
// 仿函数
func f;
std::function<bool(int, int)> f1 = f;
std::function<bool(int, int)> f2 = func1;
std::function<bool(int, int)> f3 = la;
std::function<bool(int, int)> f4 = [](int a, int b)
{ return a > b; };
// 那么我们可以直接使用包装器变量加()从而实现函数的相同效果!!!
f1(4, 5);
f2(4, 5);
f3(4, 5);
f4(4, 5);
// 你还可以发现,f1巴拉巴拉的类型都是一样的!!!
// 所以:
std::vector<std::function<bool(int, int)>> v{f1, f2, f3, f4};
// 秒懂!!!
// 那么除了像上面这么使用之外
// 我们还可以对function巴拉巴拉进行重命名为一个名字
// 那么之后该名字的变量就可以接收任何和function巴拉巴拉相同返回值,相同形参类型及个数的函数了!!!
// 这个才是我们最经常使用的!!!
using func_t = std::function<bool(int, int)>;
func_t funcc;
funcc = [](int a, int b)
{ return b > a; };
// 秒懂!!!
}
// 那么还有一个注意事项就是,要是我们要用function包装器去包装类内的静态成员函数或者是成员函数的时候
// 我们要指明类域,并且还要加上&才行
// 然后就是要注意普通成员函数还有个隐藏形参this哦
class Plus
{
public:
Plus(int n = 10)
: _n(n)
{
}
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
// 包装静态成员函数
// 成员函数要指定类域并且前面加&才能获取地址
function<int(int, int)> f4 = &Plus::plusi;
cout << f4(1, 1) << endl;
// 包装普通成员函数
// 普通成员函数还有一个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以
function<double(Plus *, double, double)> f5 = &Plus::plusd;
Plus pd;
cout << f5(&pd, 1.1, 1.1) << endl;
// 直接传类名字也行
function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(pd, 1.1, 1.1) << endl;
cout << f6(pd, 1.1, 1.1) << endl;
// 传类的右值引用也行
function<double(Plus &&, double, double)> f7 = &Plus::plusd;
cout << f7(move(pd), 1.1, 1.1) << endl;
cout << f7(Plus(), 1.1, 1.1) << endl;
return 0;
}
// OK,最后我们来讲讲bind的使用!!!
// simple(1)
// template <class Fn, class… Args>
// /* unspecified */ bind (Fn&& fn, Args&&… args);
// with return type (2)
// template <class Ret, class Fn, class… Args>
// /* unspecified */ bind (Fn&& fn, Args&&… args);
// 但是说实话,bind的使用场景比较少
// 它的最主要功能就是使用bind去绑定一些固定参数
/*
一、std::bind 的核心定位:“函数适配器”
std::bind 是 C++11 引入的函数模板(位于<functional>头文件),
本质是可调用对象包装器,更形象的称呼是 “函数适配器”——
就像生活中适配不同插头的电源转换器,
它能对现有的可调用对象(普通函数、仿函数、lambda、函数指针、类成员函数等)
做 “参数层面的改造”,无需重写原对象,就能生成一个新的可调用对象;
调用这个新对象时,会自动调用原对象,并按我们预设的规则传递参数。
二、std::bind 的调用形式(逐字拆解)
调用 std::bind 的通用格式为:auto newCallable = bind(callable, arg_list);,
各部分的通俗含义如下:
callable:待适配的原可调用对象,比如一个需要调整参数的函数、lambda 表达式等;
arg_list:逗号分隔的参数列表,对应原可调用对象的参数,包含两种类型:
普通值(如数字 10、变量 x、字符串 "test"):
表示 “固定传给原对象的参数”,调用新对象时无需再传这部分参数;
占位符(如_1、_2、_3):表示 “新对象的参数位置标记”,
调用新对象时传入的参数会按占位符的标记,填充到原对象的对应位置;
newCallable:bind 返回的新可调用对象,
调用它就等同于调用原对象,但参数已按 arg_list 的规则调整完毕。
三、占位符(_n):bind 的核心关键
占位符是实现参数调整的核心,需明确 3 个基础规则:
形式与含义:占位符以_n表示(n 是正整数),
_1代表 “调用新对象时传入的第 1 个参数”,
_2代表 “调用新对象时传入的第 2 个参数”,以此类推;
命名空间:占位符定义在std::placeholders命名空间中,
使用时需显式引用(如std::placeholders::_1)
或提前声明using namespace std::placeholders;;
作用:占位符是 “参数位置的标记”——
标记新对象的参数要传递给原对象的哪个参数位置,
比如原对象需要 3 个参数,bind 时写bind(原对象, _2, 10, _1),
表示原对象的第 1 个参数接收新对象的第 2 个参数,
第 2 个参数固定为 10,第 3 个参数接收新对象的第 1 个参数。
四、std::bind 的核心功能(参数调整)
1. 固定参数(减少参数个数)
原可调用对象需要多个参数,
通过 bind 固定其中部分参数为具体值,新对象只需传递剩下的参数,实现参数个数的减少。
例如:原函数是 “计算 a+b+c”(需 3 个参数),
通过 bind 固定 a=10,新对象只需传 b 和 c 两个参数 ——
调用新对象时,会自动把固定的 10 和传入的 b、c 一起传给原函数,无需手动传 a。
2. 调整参数顺序
原可调用对象的参数有固定顺序,
通过 bind 调整占位符的位置,可调换传递给原对象的参数顺序,参数个数不变但顺序改变。
例如:原函数是 “打印 a 和 b(a 在前、b 在后)”
,bind 时写bind(原函数, _2, _1),
表示原函数的第 1 个参数(a)接收新对象的第 2 个参数,
原函数的第 2 个参数(b)接收新对象的第 1 个参数;
调用新对象传入 (10,20),原函数实际拿到的参数是 (20,10),实现参数顺序调换。
3. 混合调整(固定参数 + 调整顺序)
同时实现参数固定和顺序调整,是最常用的复杂场景。
例如:原函数是 “计算 a-b+c-d”(需 4 个参数),
bind 时固定 a=100、d=20,同时调整 b 和 c 的顺序
(原 b 用新对象的第 2 个参数,原 c 用新对象的第 1 个参数);
调用新对象传入 (30,40),原函数实际拿到的参数是 (100,40,30,20),
既固定了部分参数,又调换了参数顺序。
五、std::bind 与 std::function 的结合
bind 返回的新可调用对象是编译器生成的匿名类型,
无法直接声明(只能用 auto 接收),
但可通过 std::function 包装,实现类型统一 ——
无论原对象是函数指针、仿函数还是 lambda,只要签名一致(返回值,参数类型及个数都相同),
bind 后的新对象都能被同一类型的 std::function 存储、传递
(比如存入同一容器,或作为统一的函数参数),弥补了 bind 返回类型匿名的不足。
六、使用 std::bind 的注意事项(避坑点)
占位符不能超界:如果新对象只接收 2 个参数,
使用_3会导致编译报错,占位符的数值不能超过新对象的参数个数;
绑定类成员函数:需额外传递类的实例(或实例指针),
比如绑定类的成员函数时,要把对象指针作为 arg_list 的第一个参数,否则无法调用;
绑定引用参数:如果想给原对象传递引用(而非拷贝),
不能直接传变量(bind 会默认拷贝变量),
需用std::ref()(非 const 引用)或std::cref()(const 引用)包装变量;
避免空绑定:如果绑定的原可调用对象失效(比如绑定了局部函数的指针、已销毁对象的成员函数),
调用新对象会导致程序出现未定义行为;
与 lambda 的对比:简单的参数调整(如固定 1-2 个参数)用 lambda 更简洁直观;
复杂的参数适配(如动态调整参数顺序、批量固定参数)用 bind 更灵活。
七、核心总结
std::bind 的本质是 “函数适配器”,
核心作用是调整可调用对象的参数(固定参数减少个数、调整占位符改变顺序);
占位符_n是关键,标记新对象的参数要填充到原对象的哪个位置;
bind 返回的匿名可调用对象可通过 std::function 包装,实现类型统一;
简单场景优先用 lambda,复杂参数适配场景用 bind 更高效。
*/
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub(int a, int b)
{
return (a – b) * 10;
}
int SubX(int a, int b, int c)
{
return (a – b – c) * 10;
}
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
auto sub1 = bind(Sub, _1, _2);
cout << sub1(10, 5) << endl;
// bind 本质返回的一个仿函数对象
// 调整参数顺序(不常用)
// _1代表第一个实参
// _2代表第二个实参
// …
auto sub2 = bind(Sub, _2, _1);
cout << sub2(10, 5) << endl;//把新对象的第二个参数传给原函数的第一个参数……
// 调整参数个数 (常用)
auto sub3 = bind(Sub, 100, _1);
cout << sub3(5) << endl;
auto sub4 = bind(Sub, _1, 100);
cout << sub4(5) << endl;
// 分别绑死第123个参数
auto sub5 = bind(SubX, 100, _1, _2);
cout << sub5(5, 1) << endl;
auto sub6 = bind(SubX, _1, 100, _2);
cout << sub6(5, 1) << endl;
auto sub7 = bind(SubX, _1, _2, 100);
cout << sub7(5, 1) << endl;
// 成员函数对象进行绑死,就不需要每次都传递了
function<double(Plus &&, double, double)> f6 = &Plus::plusd;
Plus pd;
cout << f6(move(pd), 1.1, 1.1) << endl;
cout << f6(Plus(), 1.1, 1.1) << endl;
// bind一般用于,绑死一些固定参数
function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f7(1.1, 1.1) << endl;
// 计算复利的lambda
auto func1 = [](double rate, double money, int year) -> double
{
double ret = money;
for (int i = 0; i < year; i++)
{
ret += ret * rate;
}
return ret – money;
};
// 绑死一些参数,实现出支持不同年华利率,不同金额和不同年份计算出复利的结算利息
function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);
function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);
function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);
function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);
cout << func3_1_5(1000000) << endl;
cout << func5_1_5(1000000) << endl;
cout << func10_2_5(1000000) << endl;
cout << func20_3_5(1000000) << endl;
return 0;
}
//bind的使用还是比较简单的,大家知道就行
}
结语:以优雅工具为翼,解锁现代 C++ 的可调用对象之美
亲爱的读者,当你读完这篇博客时,想必已经对 C++11 中 Lambda 表达式、std::function 包装器与 std::bind 适配器这三大核心工具,建立起了系统而深入的认知。这趟学习之旅或许曾让你在 Lambda 的捕捉列表中徘徊,在 std::function 的类型匹配中困惑,在 std::bind 的占位符规则中纠结,但请相信,每一次对细节的打磨、每一次对原理的追问,都在为你的 C++ 功底注入新的力量 —— 这些看似复杂的特性,本质上是现代 C++ 为开发者量身打造的 “优雅利器”,旨在用简洁的语法解决传统编程中的繁琐难题。
回顾 C++11 之前的编程场景,我们为了实现一个简单的排序规则,不得不编写完整的仿函数类;为了存储不同类型的可调用对象,不得不设计复杂的模板特化;为了调整函数参数顺序,不得不重写大量重载函数。这些重复而机械的工作,不仅消耗着开发者的精力,更让代码变得冗余、晦涩,难以维护。而 Lambda、std::function 与 std::bind 的出现,彻底颠覆了这一局面 —— 它们从 “轻量级函数实现”“可调用对象类型统一”“函数参数灵活适配” 三个维度,为可调用对象的处理提供了一站式解决方案,让 C++ 编程变得更加简洁、灵活、高效。
Lambda 表达式作为这三大工具的 “基石”,其核心魅力在于 “轻量与灵活”。它以匿名函数对象的形式,允许我们在函数内部直接定义逻辑,无需额外声明类或函数;其独特的捕捉列表,打破了普通函数无法直接使用外层局部变量的限制,让代码逻辑更加连贯。从底层原理来看,Lambda 并非全新的语言特性,而是编译器自动生成的仿函数类的 “语法糖”—— 但这层语法糖却意义非凡,它让我们摆脱了手动编写仿函数的繁琐流程,用最简洁的语法实现最核心的逻辑。无论是 STL 算法中的自定义排序规则、多线程中的任务逻辑,还是智能指针的自定义删除器,Lambda 都能以 “一行代码” 的姿态优雅胜任,成为现代 C++ 中处理轻量级逻辑的首选。
如果说 Lambda 是 “灵活的执行者”,那么 std::function 就是 “统一的管理者”。在 C++ 中,可调用对象的类型碎片化问题曾长期困扰着开发者:普通函数的类型由签名决定,仿函数的类型是自定义类类型,Lambda 的类型是编译器生成的匿名类型。这种类型差异导致我们无法用统一的方式存储、传递不同类型的可调用对象 —— 而 std::function 的出现,恰好打破了这一壁垒。作为一种类型安全的通用包装器,它能够包装任意符合特定签名的可调用对象,无论其底层是普通函数、仿函数、Lambda,还是类成员函数,都能被统一为同一个 std::function 类型。这种类型统一的特性,让我们可以将多个不同类型的可调用对象存储在同一个容器中,也可以将其作为函数参数或返回值灵活传递,极大地提升了代码的可维护性与扩展性。尤其是在处理回调函数、事件驱动编程等场景时,std::function 让代码结构变得清晰而规整。
而 std::bind 则是 “灵活的适配器”,它为函数参数的调整提供了优雅的解决方案。在实际开发中,我们常常遇到 “函数参数过多但只需使用部分”“参数顺序不符合需求” 等场景 —— 在 C++11 之前,这些问题只能通过重写函数重载版本来解决,不仅冗余,还容易出错。而 std::bind 通过 “固定参数”“调整参数顺序”“混合适配” 等功能,无需修改原函数,就能生成新的可调用对象,完美满足各种参数调整需求。其核心的占位符机制(_1、_2 等),让参数的映射关系变得直观而灵活;与 std::function 的结合使用,更能让调整后的可调用对象获得统一的类型,进一步提升代码的灵活性。虽然在简单场景下,Lambda 表达式也能实现类似的参数固定功能,但在复杂的参数适配场景中,std::bind 的优势依然不可替代。
这三大工具并非孤立存在,而是相互补充、相辅相成,构成了现代 C++ 处理可调用对象的 “铁三角”:Lambda 提供简洁的逻辑实现,std::function 提供统一的类型管理,std::bind 提供灵活的参数适配。当三者结合使用时,其威力更是相得益彰:用 Lambda 编写核心逻辑,用 std::bind 调整参数格式,再用 std::function 统一类型传递 —— 这种组合不仅能应对各种复杂的编程场景,更能让代码保持简洁、优雅、高效。例如,在异步编程中,我们可以用 Lambda 编写回调逻辑,用 std::bind 固定上下文参数,再用 std::function 将回调函数传递给异步接口,整个流程清晰而高效。
学习这三大特性的过程,也是培养 “现代 C++ 思维” 的过程。现代 C++ 的设计哲学之一,就是 “用语法糖简化复杂逻辑,用类型安全保证代码可靠,用灵活接口提升开发效率”—— 而 Lambda、std::function 与 std::bind 正是这一哲学的集中体现。在学习过程中,你可能曾为 Lambda 的捕捉列表规则感到困惑:值捕捉与引用捕捉的区别、mutable 修饰符的作用、混合捕捉的语法限制;也曾为 std::function 的类型匹配烦恼:类成员函数的包装方式、空状态的判断;还曾为 std::bind 的占位符使用纠结:占位符的顺序、引用参数的传递、与 Lambda 的选择。但这些细节的打磨,正是提升编程功底的关键 —— 每一个语法规则的背后,都蕴含着编译器的实现逻辑与语言设计者的考量;每一次调试错误的经历,都能让你对特性的理解更加深刻。
请记住,编程的本质是解决问题,而工具的价值在于让解决问题的过程更加高效、优雅。Lambda、std::function 与 std::bind 之所以能成为现代 C++ 的核心工具,正是因为它们精准地击中了开发者的痛点,让原本繁琐的工作变得简单。但工具的威力能否充分发挥,关键在于使用者是否能熟练掌握其用法、理解其原理。因此,在学习之后,不妨多在实际项目中实践:尝试用 Lambda 替代原本的仿函数,用 std::function 统一回调函数类型,用 std::bind 调整参数适配场景。只有在实践中不断摸索、总结,才能真正将这些工具内化为自己的能力,让它们成为你编程工具箱中最可靠的 “伙伴”。
C++ 是一门不断演进的语言,从 C++11 到 C++23,每一个版本都在引入新的特性,让语言变得更加强大、更加易用。Lambda、std::function 与 std::bind 作为 C++11 的核心特性,不仅是现代 C++ 编程的基础,更是通往更高阶特性的桥梁。掌握了它们,你将能更轻松地理解后续版本中的新特性,如 C++17 的 std::invoke、C++20 的概念与范围库等。学习 C++ 的道路或许漫长,但每一次对新特性的掌握,每一次代码质量的提升,都是对自己的肯定与激励。
最后,愿你以这三大工具为翼,在现代 C++ 的世界中自由驰骋 —— 用 Lambda 的简洁勾勒逻辑,用 std::function 的统一管理类型,用 std::bind 的灵活适配参数,写出更加优雅、高效、可维护的代码。不要畏惧学习中的难点,因为每一次突破都将让你变得更加强大;不要满足于表面的用法,因为深入原理才能真正掌握本质。愿你保持对技术的热爱与好奇心,在 C++ 的学习之路上不断前行,解锁更多语言的魅力,实现自己的编程理想。
祝你在现代 C++ 的编程旅程中,收获满满,一路繁花!
网硕互联帮助中心



![第5章,[Win32 章节] :边框绘制函数(二)-网硕互联帮助中心](https://www.wsisp.com/helps/wp-content/uploads/2026/08/20260811231700-6a7bad6c42588-220x150.png)

评论前必须登录!
注册