
大家好,欢迎来到 huangjin007_ 的博客
⭐ 个人主页:huangjin007_
🔥 文章收录专栏:零基础入门C++
总会有一些坚持
能从冰封的土地里
培育出十万朵怒放的蔷薇
C++11篇(四) ------ 新的类功能、lambda 与包装器详解
本篇文章将由浅入深,带你循序渐进地掌握C++11中的新的类功能、lambda 与包装器,全程干货,坐稳发车~ ദ്ദി˶ー̀֊ー́ )✧
文章目录
- [C++11篇(四) ------ 新的类功能、lambda 与包装器详解](#C++11篇(四) —— 新的类功能、lambda 与包装器详解)
-
- [1. 新的类功能:默认成员函数的演化](#1. 新的类功能:默认成员函数的演化)
-
- [1.1 回顾 C++98 的 6 个默认成员函数](#1.1 回顾 C++98 的 6 个默认成员函数)
- [1.2 C++11 新增的默认移动构造和移动赋值](#1.2 C++11 新增的默认移动构造和移动赋值)
- [1.3 类内成员变量声明时直接给缺省值](#1.3 类内成员变量声明时直接给缺省值)
- [1.4 `default` 和 `delete`:精确控制默认函数](#1.4
default和delete:精确控制默认函数) - [1.5 `final` 与 `override` (简要回顾)](#1.5
final与override(简要回顾))
- [2. STL 中的一些变化(概览)](#2. STL 中的一些变化(概览))
- [3. lambda 表达式](#3. lambda 表达式)
-
- [3.1 lambda 表达式语法全貌](#3.1 lambda 表达式语法全貌)
- [3.2 捕捉列表详解(重点)](#3.2 捕捉列表详解(重点))
-
- [3.2.1 显式捕捉](#3.2.1 显式捕捉)
- [3.2.2 隐式捕捉](#3.2.2 隐式捕捉)
- [3.2.3 混合捕捉](#3.2.3 混合捕捉)
- [3.2.4 不能捕捉的变量](#3.2.4 不能捕捉的变量)
- [3.2.5 mutable 关键字](#3.2.5 mutable 关键字)
- [3.3 lambda 的典型应用:轻松替代仿函数](#3.3 lambda 的典型应用:轻松替代仿函数)
- [3.4 lambda 的底层原理:仿函数对象](#3.4 lambda 的底层原理:仿函数对象)
- [4. 包装器:统一可调用对象的利器](#4. 包装器:统一可调用对象的利器)
-
- [4.1 `std::function`:万能的函数包装器](#4.1
std::function:万能的函数包装器) -
- [4.1.1 包装各种可调用对象](#4.1.1 包装各种可调用对象)
- [4.1.2 包装静态成员函数与普通成员函数](#4.1.2 包装静态成员函数与普通成员函数)
- [4.1.3 应用实例:使用 `function` 解决逆波兰表达式求值](#4.1.3 应用实例:使用
function解决逆波兰表达式求值)
- [4.2 `std::bind`:灵活的参数适配器](#4.2
std::bind:灵活的参数适配器) -
- [4.2.1 基本用法](#4.2.1 基本用法)
- [4.2.2 绑定固定参数,调整参数个数](#4.2.2 绑定固定参数,调整参数个数)
- [4.2.3 bind 绑定成员函数与对象](#4.2.3 bind 绑定成员函数与对象)
- [4.2.4 实际场景:用 bind 定制利率计算器](#4.2.4 实际场景:用 bind 定制利率计算器)
- [4.1 `std::function`:万能的函数包装器](#4.1
- 结语:
1. 新的类功能:默认成员函数的演化
1.1 回顾 C++98 的 6 个默认成员函数
在传统 C++ 中,如果我们定义一个类而不写某些特殊函数,编译器会在需要时自动生成它们。经典的说法是 "6 个默认成员函数":
- 默认构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符重载
- 取地址运算符重载
- const 取地址运算符重载
实际开发中,真正重要的就是前四个。后两个默认取地址函数基本不会被刻意重载,所以大多数时候我们只关心构造、析构、拷贝构造和拷贝赋值。
C++11 在此基础上引入了移动语义,因此编译器也学会了自动生成与移动相关的函数。
1.2 C++11 新增的默认移动构造和移动赋值
从 C++11 开始,类的默认成员函数变成了 8 个,新增的两个是:
- 移动构造函数
- 移动赋值运算符重载
但注意:编译器并不会无脑地总是自动生成它们,它的生成条件相当讲究。规则如下:
如果你没有自己实现移动构造函数,而且也没有自己实现析构函数、拷贝构造函数、拷贝赋值重载中的任意一个 ,编译器就会自动生成一个默认的移动构造函数。
同样的规则也适用于移动赋值:
如果你没有自己实现移动赋值函数,而且也没有自己实现析构函数、拷贝构造函数、拷贝赋值重载中的任意一个 ,编译器就会自动生成一个默认的移动赋值运算符。
换句通俗的话讲:一旦你手动干预了"资源管理"相关的那几个函数(析构、拷贝构造、拷贝赋值),编译器就认为你对资源的掌控有特殊需求,因此它不再敢擅自帮你生成移动操作,以免破坏你的意图。 反之,如果你完全没碰那些函数,编译器就放心地默认生成移动构造和移动赋值。
那么,默认生成的移动构造和移动赋值具体做些什么呢?
- 对于内置类型 成员(如 int、double、指针),默认移动执行的是逐成员按字节拷贝(效果和拷贝一样,因为内置类型没有"移动"的概念)。
- 对于自定义类型 成员,编译器会查看这个成员有没有实现移动构造/移动赋值。如果实现了,就调用它的移动构造/移动赋值;如果没有实现,则退一步调用它的拷贝构造/拷贝赋值。
这样,最终整个对象的"移动"效果就是:能移动的成员就移动,不能移动的就拷贝。对于标准库里的 std::string、std::vector 等,它们都实现了移动语义,所以如果你的类包含这些成员,默认生成的移动操作将会非常高效地把资源"窃取"过来。
代码示例:观察默认移动构造的生成
我们先定义一个非常简单的 Person 类,里面包含一个自定义的 hj::string(你可以把它想象成 std::string)和一个 int 成员。
cpp
class Person
{
public:
Person(const char* name = "张三", int age = 1)
:_name(name)
,_age(age)
{ }
// 注意:这里没有写析构、拷贝构造、拷贝赋值
// 因此编译器会自动生成移动构造和移动赋值
private:
hj::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1; // 调用拷贝构造(没问题)
Person s3 = std::move(s1); // 调用编译器自动生成的移动构造
Person s4;
s4 = std::move(s2); // 调用编译器自动生成的移动赋值
return 0;
}

上面的代码中:
Person s2 = s1;使用拷贝构造,把s1的姓名和年龄复制给s2。由于我们没有写拷贝构造,编译器默认生成,它会对_name调用hj::string的拷贝构造,对_age逐字节拷贝。Person s3 = std::move(s1);触发移动构造。因为我们没有干预资源管理函数,编译器生成了默认移动构造。移动构造会对_name调用hj::string的移动构造,高效地"偷"走s1内部字符串的指针,而不是重新申请内存复制一份。对于_age则依然是直接拷贝。std::move只是将左值强制转换为右值引用,本身不做移动,目的是告诉编译器"请尝试移动这个对象"。s4 = std::move(s2);同理,调用默认移动赋值。
面试重点 :如果我们在类里手动实现析构函数,或者拷贝构造/拷贝赋值中的任何一个,编译器就不再生成默认移动构造和移动赋值。
另外,C++11 还有一个对称规则:如果你自己提供了移动构造或移动赋值,编译器就不会再自动提供拷贝构造和拷贝赋值。 也就是说,移动和拷贝的自动生成在一定程度上是互斥的。
1.3 类内成员变量声明时直接给缺省值
这个特性其实我们在学习类和对象的时候就接触过,这里简单提一下以免遗漏:
cpp
class Person
{
private:
hj::string _name = "匿名";
int _age = 18;
};
成员变量在声明时直接赋初值,其本质是为初始化列表服务的。当你的构造函数没有在初始化列表中显式初始化某个成员时,编译器就会用这个缺省值去初始化它。比如:
cpp
Person()
{} // _name 会被初始化为 "匿名",_age 初始化为 18
Person(const char* name)
:_name(name) // _age 会使用缺省值 18
{}
这个特性可以大大减少构造函数的重复代码,也让成员初始值更加直观。
1.4 default 和 delete:精确控制默认函数
有时我们会面临一个两难的境地:因为某些特殊原因,我们不得不手动写一个拷贝构造函数(比如需要深拷贝),但这却导致编译器不再生成移动构造函数。如果我们确信移动构造的默认行为是没问题的,怎么办呢?
C++11 提供了 default 关键字,它可以显式告诉编译器:"请为我生成这个函数的默认版本。"
cpp
class Person
{
public:
Person(const char* name = "张三", int age = 1)
:_name(name)
, _age(age)
{}
// 因为我们写了拷贝构造,移动构造就不会自动生成了
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
// 使用 default 显式要求编译器生成默认移动构造
Person(Person&& p) = default;
private:
hj::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1; // 调用我们写的拷贝构造
Person s3 = std::move(s1); // 调用 default 生成的移动构造
return 0;
}

通过 = default,我们既保留了手写的拷贝构造,又获得了高效的默认移动构造。这对于保持性能非常重要。
与 default 相反,delete 关键字用于禁止某个函数的生成或使用。在 C++98 时代,如果想禁止拷贝,通常是把拷贝构造函数声明为 private 且不实现。C++11 可以直接在声明后面加上 = delete,意图更清晰:
cpp
class NonCopyable
{
public:
NonCopyable() = default;
// 禁止拷贝
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
一旦尝试拷贝,编译器就会直接报错,错误信息非常明确。
1.5 final 与 override (简要回顾)
这两个关键字主要用于继承和多态场景,此处不再详细展开。
final:修饰类表示不可被继承 ,修饰虚函数表示不可在派生类中被重写。override:显式声明该函数重写了基类的虚函数,编译器会检查签名是否一致,避免因为参数不同而变成"新建了一个函数"。
如果忘了,可以看看博主之前写的文章。
2. STL 中的一些变化(概览)
C++11 给标准模板库也带来了大量新东西,主要包括:
- 新容器 :
unordered_map、unordered_set、array、forward_list等。其中unordered_map/unordered_set是哈希表实现的关联容器,查找平均 O(1),非常重要。 - 新接口 :大量容器新增了与右值引用和移动语义相关的成员函数,如
push_back(T&&)、emplace_back(Args&&...)、移动构造、移动赋值,以及接受initializer_list的构造函数。 - 简化写法:支持范围 for 循环遍历容器。
本文的重点不是 STL,所以这部分只看个大概,知道有这些变化就行。
3. lambda 表达式
3.1 lambda 表达式语法全貌
lambda 表达式可以理解为可以在函数内部定义的匿名函数对象。它最吸引人的地方就是简洁------不需要单独定义一个函数或者仿函数类,三两行代码就能写一个可调用的逻辑。
先看一个最简单完整的 lambda:
cpp
auto add1 = [](int x, int y) -> int { return x + y; };
cout << add1(1, 2) << endl; // 输出 3
lambda 表达式的完整语法结构如下:
[capture-list] (parameters) -> return-type { function-body }
各部分详解:
[capture-list]:捕捉列表。编译器通过[]来识别"这是一个 lambda 表达式"。它用来捕捉当前作用域中的局部变量,让函数体可以使用。即使不捕捉任何变量,方括号也不能省略。(parameters):参数列表,和普通函数参数一样。如果不需要参数,可以连同括号一起省略。-> return-type:返回值类型,采用尾置返回类型语法。如果编译器可以推导出返回类型,或者没有返回值,则可以省略->及后面的部分。{ function-body }:函数体,和普通函数完全一样。函数体不能省略,但可以为空{}。
举几个省略的例子:
cpp
// 最简形式:无参数、无返回值(自动推导为 void),只是打印
auto func1 = []
{
cout << "hello world" << endl;
};
func1();
// 参数和返回值都可以被推导
int a = 0, b = 1;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(a, b);
cout << a << ":" << b << endl; // 输出 1:0
在 swap1 中我们没有写 ->,但编译器可以自动推导出返回类型为 void。
3.2 捕捉列表详解(重点)
lambda 的默认规则是:函数体中只能使用其自身的参数、函数体内定义的变量、全局变量和静态局部变量,不能直接使用外层作用域的局部变量。如果想用,就得通过捕捉列表"捕获"进来。
捕捉方式有三种基本形态:
- 显式捕捉:手动列出要捕捉的变量,并指定传值还是传引用。
- 隐式捕捉 :用
=或&让编译器自动捕捉。- 混合捕捉:隐式捕捉 + 部分变量特殊处理。
3.2.1 显式捕捉
cpp
int a = 0, b = 1;
auto func1 = [a, &b]
{
// a 是值捕捉,默认不可修改
// b 是引用捕捉,可以修改,且会影响外部的 b
b++;
return a + b;
};
cout << func1() << endl;
[a, &b]表示a传值捕捉,b传引用捕捉。- 值捕捉进来的变量在 lambda 内部默认是
const的,所以不能执行a++。引用捕捉没有这个限制,修改会影响原变量。
3.2.2 隐式捕捉
cpp
int a = 0, b = 1, c = 2, d = 3;
// 隐式值捕捉:用 =,所有在函数体内出现的外部变量都会被值捕捉
auto func2 = [=]
{
return a + b + c; // 编译器自动捕获 a, b, c,均为值
};
cout << func2() << endl;
// 隐式引用捕捉:用 &,所有出现的外部变量都会被引用捕捉
auto func3 = [&]
{
a++; b++; c++; // 可以直接修改外层变量
};
func3();
cout << a << " " << b << " " << c << " " << d << endl;
隐式捕捉的好处是不用逐个写变量名,但要注意值捕捉和引用捕捉的本质区别:值捕捉是拷贝,修改不影响外部;引用捕捉操作的是原变量,修改会同步到外部。
3.2.3 混合捕捉
混合捕捉格式中,第一个位置必须是 & 或 =,用来指定默认捕捉方式。然后可以在后面显式指定某些变量采取相反的方式。
cpp
int a = 1, b = 2, c = 3, d = 4;
// 默认引用捕捉,但 a 和 b 用值捕捉
auto func4 = [&, a, b]
{
// a++; // 错误,a 是值捕捉,不可修改
// b++; // 错误
c++; // 引用捕捉,可以
d++;
return a + b + c + d;
};
func4();
cout << a << " " << b << " " << c << " " << d << endl;
// 默认值捕捉,但 a 和 b 用引用捕捉
auto func5 = [=, &a, &b]
{
a++;
b++;
// c++; // 错误,c 是值捕捉,不可修改
return a + b + c + d;
};
func5();
cout << a << " " << b << " " << c << " " << d << endl;
规则简单概括:
[&, a, b]:除了a、b是值捕捉,其余都是引用捕捉。[=, &a, &b]:除了a、b是引用捕捉,其余都是值捕捉。
3.2.4 不能捕捉的变量
- 全局变量 和 静态局部变量 不能被捕捉,也没必要捕捉,因为它们在 lambda 函数体内可以直接使用。请看代码:
cpp
int x = 0; // 全局变量
int main()
{
static int m = 0; // 静态局部变量
auto func6 = []
{
// 直接使用 x 和 m,不用捕捉
int ret = x + m;
return ret;
};
// ...
return 0;
}
另外,如果 lambda 本身定义在全局作用域,那么它的捕捉列表必须是空的,因为没有可以捕捉的外部局部变量。
cpp
int x = 0;
auto func1 = []() // 只能为空
{
x++; // 直接操作全局变量
};
3.2.5 mutable 关键字
前面提到,值捕捉的变量在 lambda 内部是 const 的,不能修改。如果我们确实需要在 lambda 内部修改一份拷贝,而不影响外部,可以使用 mutable。
cpp
int a = 1, b = 2, c = 3, d = 4;
auto func7 = [=]() mutable // 去掉 const 限制
{
a++; b++; c++; d++; // 现在都可以修改了
return a + b + c + d;
};
cout << func7() << endl; // 内部修改的是拷贝,总和变大了
cout << a << " " << b << " " << c << " " << d << endl; // 外面原值不变

注意:一旦使用了 mutable,即使参数列表为空,括号也不能省略。
3.3 lambda 的典型应用:轻松替代仿函数
在没有 lambda 的时代,如果我们想给 sort 传入自定义的比较规则,要么写一个普通函数,要么定义一个仿函数类。仿函数的写法比较啰嗦:
cpp
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;
}
};
使用时得这样:
cpp
vector<Goods> v = { {"苹果", 2.4, 5}, {"香蕉", 3, 4}, {"橙子", 2.7, 3}, {"菠萝", 1.5, 4} };
sort(v.begin(), v.end(), ComparePriceLess()); // 价格升序
sort(v.begin(), v.end(), ComparePriceGreater()); // 价格降序
但有了 lambda,就可以直接在调用处写出逻辑,代码非常紧凑:
cpp
// 价格升序
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;
});
一对比就能明显感受到,lambda 减少了多少样板代码!
3.4 lambda 的底层原理:仿函数对象
你可能好奇:这么神奇的 lambda,到底是怎么实现的?事实上,C++ 编译器在背后根本不认识"lambda"这个东西 ,它做的事情是把 lambda 翻译成一个普通的仿函数类。
我们拿一个具体例子来看:
cpp
// 原始仿函数类
class Rate
{
public:
Rate(double rate)
: _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
double rate = 0.49;
// lambda 等价物
auto r2 = [rate](double money, int year)
{
return money * rate * year;
};
Rate r1(rate); // 构造仿函数对象
r1(10000, 2); // 调用仿函数
r2(10000, 2); // 调用 lambda,看起来完全一样
auto func1 = [] { cout << "hello world" << endl; };
func1();
return 0;
}
编译器在遇到 [rate](double money, int year) { ... } 时,大致会生成这样一个类(名字由编译器自己生成,保证唯一性):
cpp
class __lambda_1
{
public:
__lambda_1(double rate)
: rate(rate)
{}
double operator()(double money, int year) const
{
return money * rate * year;
}
private:
double rate;
};
我们会发现:
- 捕捉列表里的变量 (这里是
rate)变成了这个类的成员变量,并且通过构造函数初始化。- lambda 的参数和返回值 变成了
operator()的参数和返回值。- lambda 的函数体 变成了
operator()的实现。- 默认情况下,该
operator()是const成员函数,所以值捕捉的变量不能被修改。如果你加了mutable,编译器就去掉const修饰。
这个原理也可以通过查看汇编代码得到印证:你会看到编译器生成的各种带 <lambda> 字样的类型名,并且调用 lambda 对象实质就是调用它的 operator()。
因此,我们可以得出一个结论:lambda 只是一种简化语法,本质是编译器自动生成的仿函数对象。 了解这一点后,你就会明白为什么 lambda 可以无缝地与 std::function、STL 算法等搭配使用------因为它们的底层都是"可调用对象"。
4. 包装器:统一可调用对象的利器
在实际编程中,我们经常会遇到各种类型的可调用对象:普通函数、函数指针、仿函数对象、lambda 表达式、成员函数指针......它们虽然都能"调用",但类型却各不相同,这就给泛型编程或存储回调函数带来了麻烦。C++11 提供了两个强大的工具来统一处理它们:std::function 和 std::bind。
4.1 std::function:万能的函数包装器
std::function 是一个类模板 ,定义在 <functional> 头文件中。它能够将任意一种可调用对象(只要签名匹配)包装起来,赋予它们统一的类型。
其原型(简化理解)如下:
cpp
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数写为 Ret(Args...) 的形式,看起来像一个函数签名。例如 function<int(int, int)> 就表示包装一个接受两个 int、返回 int 的可调用对象。
4.1.1 包装各种可调用对象
cpp
#include <functional>
using namespace std;
// 普通函数
int f(int a, int b)
{
return a + b;
}
// 仿函数
struct Functor
{
int operator()(int a, int b)
{
return a + b;
}
};
int main()
{
// 包装普通函数
function<int(int, int)> f1 = f;
// 包装仿函数
function<int(int, int)> f2 = Functor();
// 包装 lambda
function<int(int, int)> f3 = [](int a, int b) { return a + b; };
cout << f1(1, 1) << endl; // 2
cout << f2(1, 1) << endl; // 2
cout << f3(1, 1) << endl; // 2
}
可以看到,无论原始类型是什么,只要可调用对象符合 int(int, int) 的签名,就可以被同一个 function<int(int, int)> 类型存储,并且调用方式一模一样。
4.1.2 包装静态成员函数与普通成员函数
成员函数稍微特殊一点,因为它隐含一个 this 指针。静态成员函数没有 this,所以和普通函数完全一样:
cpp
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;
};
// 成员函数要指定类域并且前面加 & 才能获取地址
function<int(int, int)> f4 = &Plus::plusi; // 静态成员函数
cout << f4(1, 1) << endl; // 2
对于普通成员函数 plusd,它实际有三个"参数":this(隐式)、double a、double b。因此包装时,function 的签名必须体现 this 的存在。常见的做法有两种:
cpp
// 方式1:第一个参数明确写为类指针
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus pd;
cout << f5(&pd, 1.1, 1.1) << endl; // 传入对象地址
// 方式2:第一个参数写为类对象(或引用),会拷贝或引用
function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(pd, 1.1, 1.1) << endl; // 会拷贝 pd
// 也可以绑定右值引用
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;
总结 :普通成员函数的 function 类型需要在第一个参数位置显式传递类对象(或其指针/引用),调用时也要相应地传入对象实例。
4.1.3 应用实例:使用 function 解决逆波兰表达式求值

逆波兰表达式求值是栈的经典应用。传统写法往往使用 switch 或 if-else 来判断运算符:
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for (auto& s : tokens)
{
if (s == "+" || s == "-" || s == "*" || s == "/")
{
int right = st.top(); st.pop();
int left = st.top(); st.pop();
switch (s[0])
{
case '+': st.push(left + right); break;
case '-': st.push(left - right); break;
case '*': st.push(left * right); break;
case '/': st.push(left / right); break;
}
}
else
{
st.push(stoi(s));
}
}
return st.top();
}
};
这种写法扩展性较差:如果未来要支持更多的运算符(比如取模 %、幂运算 ^),就得修改 switch 分支。使用 std::function 结合 map 可以极大地提升扩展性:
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
// 建立运算符到可调用对象的映射
map<string, function<int(int, int)>> Map = {
{"+", [](int x, int y) { return x + y; }},
{"-", [](int x, int y) { return x - y; }},
{"*", [](int x, int y) { return x * y; }},
{"/", [](int x, int y) { return x / y; }}
};
for (auto& s : tokens)
{
if (Map.count(s)) { // 是运算符
int right = st.top(); st.pop();
int left = st.top(); st.pop();
int ret = Map[s](left, right);
st.push(ret);
}
else
{
st.push(stoi(s));
}
}
return st.top();
}
};
这样,如果我们需要增加新的运算符,只需要在 Map 的初始化列表里加一行即可,完全不用修改核心逻辑。这正是 function 将"行为"作为一个值来存储所带来的灵活性。
4.2 std::bind:灵活的参数适配器
std::bind 同样定义在 <functional> 中,它是一个函数模板,可以看作是一个"函数适配器"。它接收一个可调用对象和一系列参数,返回一个新的可调用对象。在这个新对象中,一些参数可以被"固定"(绑定),另一些参数可以重新调整顺序。
4.2.1 基本用法
bind 的一般调用形式为:
cpp
auto newCallable = bind(callable, arg_list);
callable:原本的可调用对象。arg_list:逗号分隔的参数列表,可能包含具体的值,也可能包含占位符_1、_2、_3......这些占位符定义在std::placeholders命名空间内,通常我们引入using namespace std::placeholders;。
_1表示newCallable被调用时的第一个实参** ,_2表示第二个实参,以此类推。**
4.2.2 绑定固定参数,调整参数个数
cpp
int Sub(int a, int b)
{
return (a - b) * 10;
}
auto sub1 = bind(Sub, _1, _2);
cout << sub1(10, 5) << endl; // Sub(10, 5) => 50
这跟直接调用 Sub 没啥区别。但我们可以调换参数顺序 或固定某些参数:
cpp
// 调换参数顺序
auto sub2 = bind(Sub, _2, _1);
cout << sub2(10, 5) << endl; // Sub(5, 10) => -50
// 固定第一个参数
auto sub3 = bind(Sub, 100, _1);
cout << sub3(5) << endl; // Sub(100, 5) => 950
// 固定第二个参数
auto sub4 = bind(Sub, _1, 100);
cout << sub4(5) << endl; // Sub(5, 100) => -950
对于参数更多的函数,一样可以灵活固定部分参数,减少外部调用时需要传递的实参数量:
cpp
int SubX(int a, int b, int c)
{
return (a - b - c) * 10;
}
auto sub5 = bind(SubX, 100, _1, _2); // 固定了 a,剩下两个由调用者提供
cout << sub5(5, 1) << endl; // (100 - 5 - 1)*10 = 940
auto sub6 = bind(SubX, _1, 100, _2); // 固定 b
cout << sub6(5, 1) << endl; // (5 - 100 - 1)*10 = -960
auto sub7 = bind(SubX, _1, _2, 100); // 固定 c
cout << sub7(5, 1) << endl; // (5 - 1 - 100)*10 = -960
4.2.3 bind 绑定成员函数与对象
在上一节的 function 示例中,包装普通成员函数需要每次调用时都传递对象本身。bind 能够把对象也一起绑定死,从而生成一个无需再传对象的简单可调用对象:
cpp
Plus pd;
// 原来的 f6:function<double(Plus, double, double)> 调用时还要传 pd
// 使用 bind 将对象绑定
function<double(double, double)> f7 = bind(&Plus::plusd, pd, _1, _2);
cout << f7(1.1, 1.1) << endl; // 内部使用 pd 调用 plusd
也可以直接绑定临时对象或者 ref 包装的引用,十分灵活。
4.2.4 实际场景:用 bind 定制利率计算器
假设我们有一个计算复利的 lambda:
cpp
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; // 返回利息部分
};
如果我们有多个理财产品,利率和年限已经确定,只需要计算不同本金下的利息。就可以用 bind 固定利率和年限:
cpp
function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3); // 年利率1.5%,3年期
function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5); // 年利率1.5%,5年期
function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10); // 2.5%,10年
function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30); // 3.5%,30年
cout << func3_1_5(1000000) << endl; // 100万存3年利息
cout << func5_1_5(1000000) << endl; // 100万存5年利息
cout << func10_2_5(1000000) << endl;
cout << func20_3_5(1000000) << endl;
这种通过 bind 参数化固定部分参数生成新函数的手法,在函数式编程中非常常见,也让代码更容易复用。
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _