C++11从可变参数模板到bind绑定器的详细过程
作者:jinyishu_
前言:书接上回
在上一篇文章《C++11初探:C++发展史上的里程碑》中,我们从C++11的历史背景出发,一路攻克了统一初始化、右值引用、移动语义、万能引用和完美转发。这些特性已经让你手中的C++脱胎换骨。
这篇文章我们继续介绍C++11中的三个趁手工具:
- 可变参数模板:“模板的模板”,避免繁琐的函数重载
- lambda 表达式:简单好用的可调用对象
- function + bind:包装可调用对象的工具
每一招都能让你的代码更加简洁清晰。今天我们就来彻底拿下它们。
一、可变参数模板
1.1 函数重载的繁冗
假设你想写一个打印函数,能打印任意数量的参数:
// 一个参数
void Print(int x) { cout << x << endl; }
// 两个参数——再写一个
void Print(int x, string y) { cout << x << " " << y << endl; }
// 三个参数——再再写一个
void Print(int x, string y, double z) {
cout << x << " " << y << " " << z << endl;
}
// ... 四个?五个?到底写到什么时候?这种写法不仅丑陋,而且只能支持特定类型的组合。如果用户想打印 double + string + int 呢?你又要写一个重载。
C++98就这样忍了十几年。模板的发明解决了一部分问题(类型可以参数化了),但参数的个数还是写死的。
1.2 可变参数模板的出现
C++11引入了可变参数模板(Variadic Templates),允许模板接受任意数量、任意类型的参数:
template <class ...Args>
void Print(Args... args) {
// args 是一个"参数包"(parameter pack)
// 包含了你传进来的所有参数
}两个关键的语法点:
| 语法 | 位置 | 含义 |
|---|---|---|
class ...Args | 模板参数列表 | 声明一个"类型参数包",Args不是单个类型,是一组类型 |
Args... args | 函数参数列表 | 声明一个"函数参数包",args不是单个参数,是一组参数 |
类比理解:Args... 就像一个可以装任意多个类型的"麻袋";args 就像是这个麻袋里对应的实际参数。
1.3sizeof...(args)——数数到底有几个
sizeof...(args) 可以获取参数包中参数的数量,注意它返回的是编译期常量:
template <class ...Args>
void Print(Args&&... args) {
cout << "参数个数:" << sizeof...(args) << endl;
}
int main() {
Print(); // 输出:参数个数:0
Print(1); // 输出:参数个数:1
Print(1, "hello"); // 输出:参数个数:2
Print(1, 2.5, "hello"); // 输出:参数个数:3
}好,现在我们能"数"参数了,但真正的问题是——怎么取出每一个参数来用?
1.4 参数包的展开
坑1:不能用 [] 下标访问
初学者最容易想到的写法——编译不通过!:
template <class ...Args>
void Print(Args&&... args) {
// 错误示范!不支持下标访问
for (size_t i = 0; i < sizeof...(args); i++) {
cout << args[i] << " "; // ❌ 编译错误!
}
cout << endl;
}原因很简单:sizeof...(args) 虽然是编译期常量,但 args[i] 这种下标访问语法只适用于同类型的数组。而 args 里每个参数的类型都可能不一样(一个int,一个string,一个double……),编译器根本无法确认 args[i] 的类型!
坑2:不支持范围for循环
template <class ...Args>
void Print(Args&&... args) {
// 错误示范!范围for不支持参数包展开
for (auto&& e : args...) { // ❌ 同样编译不通过
cout << e << " ";
}
cout << endl;
}
范围for要求 args 是一个可迭代的容器,但参数包不是容器,编译器不知道怎么遍历它。
1.5 方法一:递归展开(最经典的方式)
可变参数包的"正统"展开方式是递归——每次取出第一个参数,然后把剩下的参数递归传给下一层:
// 递归终止函数——当参数包为空时调用
void ShowList() {
cout << endl; // 打印最后一个换行,递归结束
}
// 递归展开函数——每次取出第一个参数x,剩下的继续展开
template <class T, class ...Args>
void ShowList(T&& x, Args&&... args) {
cout << x << " "; // 处理当前层的第一个参数
ShowList(args...); // 剩下的参数包递归传给下一层
}
// 对外接口——直接转发给实际的处理函数
template <class ...Args>
void Print(Args&&... args) {
ShowList(args...);
}
int main() {
Print(1, 2.5, string("hello"), 'A');
// 输出:1 2.5 hello A
return 0;
}来看看这段代码是怎么一层层展开的:
调用 Print(1, 2.5, "hello", 'A')
↓
调用 ShowList(1, 2.5, "hello", 'A')
x = 1, 打印 "1 "
args = (2.5, "hello", 'A')
↓ 递归
调用 ShowList(2.5, "hello", 'A')
x = 2.5, 打印 "2.5 "
args = ("hello", 'A')
↓ 递归
调用 ShowList("hello", 'A')
x = "hello", 打印 "hello "
args = ('A')
↓ 递归
调用 ShowList('A')
x = 'A', 打印 "A "
args = () ← 空参数包
↓ 递归
调用 ShowList() ← 匹配到终止函数,打印换行关键点:
- 每次递归参数减少一个,直到参数包为空
- 终止函数
ShowList()是必须的,否则当参数包为空时编译器找不到匹配的函数。另外,你可能会想到在ShowList的函数体中写递归终止条件,这是不可行的,因为C++ 模板参数包是编译期语法,不能用运行时 if 判断参数包是否为空。 - 这里用了
T&&万能引用,配合引用折叠,完美接收各种值类别
1.6 利用函数参展开
递归展开虽然经典,但需要写两个函数,稍显啰嗦。如果你只是想把参数包里的每个元素"过一遍"(比如打印),可以利用函数参数来展开:
template <class T>
int GetArg(const T& x) {
cout << x << " ";
return 0;
}
// 这个函数什么都不做,只是用来"容纳"逗号表达式展开的结果
template <class ...Args>
void Arguments(Args... args) {}
template <class ...Args>
void Print(Args... args) {
// 核心魔法在这一行!
Arguments(GetArg(args)...);
}
int main() {
double x = 2.2;
Print();
Print(1);
Print(1, string("xxxxx"));
Print(1.1, string("xxxxx"), x);
return 0;
}等一下,这一行到底发生了什么?
Arguments(GetArg(args)...);
这个语法 GetArg(args)... 叫做参数包展开,注意 ... 的位置——它在 GetArg(args) 的外面。意思是:把参数包中的每个参数依次传给 GetArg,得到一组返回值,再把这组返回值作为 Arguments 的实参。
举个例子,Print(1, string("hello"), 2.2) 展开后等价于:
Arguments(GetArg(1), GetArg(string("hello")), GetArg(2.2));GetArg 每次被调用都会打印一个参数并返回0(返回值其实无所谓,只是为了凑够传给 Arguments 的实参)。而 Arguments 什么都不做,它的唯一作用就是——作为一个"接收器",让逗号表达式能够"合法"地展开。
1.7 可变参数模板 + 完美转发
现在我们把可变参数模板和上一篇文章学的完美转发结合起来。
看看 STL 中 list 容器的节点构造函数:
template<class T>
struct list_node {
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node() = default;
// 可变参数 + 万能引用 + 完美转发 → 三合一
template <class... Args>
list_node(Args&&... args)
: _next(nullptr)
, _prev(nullptr)
, _data(std::forward<Args>(args)...) // 注意这里...
{}
};注意 std::forward<Args>(args)... 这行,同样要把 ... 放在外面,表示"对参数包里的每个参数都执行 std::forward"。展开后类似:
_data(std::forward<T1>(arg1), std::forward<T2>(arg2), std::forward<T3>(arg3))
这样就能把每个参数以及它的"左值/右值"属性原封不动地转发给 _data 的构造函数。
二、emplace_back 如何碾压 push_back
2.1 回顾 push_back 的两种重载
在深挖 emplace_back 之前,先回顾一下我们学过的 push_back:
template<class T>
class list {
public:
// 左值引用版本:深拷贝
void push_back(const T& x) {
insert(end(), x);
}
// 右值引用版本:移动
void push_back(T&& x) {
insert(end(), forward<T>(x));
}
};调用场景:
jys::list<jys::string> lt;
jys::string s1("111111111111");
lt.push_back(s1); // s1是左值 → 调用 const T& 版本
lt.push_back(move(s1)); // move后是右值 → 调用 T&& 版本
lt.push_back("111111111111"); // 字面量是右值 → 调用 T&& 版本,但需要隐式构造string注意第三种情况:push_back("111111111111") 虽然是右值,但 需要先用 "111111111111" 构造一个临时的 string 对象,再把这个临时对象移动到 list 节点中。多了一步隐式构造+移动。
2.2 emplace_back:直接节点中原地构造
emplace_back 的核心优势在于直接在节点的内存空间上构造对象,彻底消灭了"先构造再拷贝/移动"的中间步骤。
template<class T>
class list {
public:
// emplace_back 接受任意参数,直接转发给节点构造函数
template <class... Args>
void emplace_back(Args&&... args) {
insert(end(), std::forward<Args>(args)...);
}
template <class... Args>
iterator insert(iterator pos, Args&&... args) {
Node* cur = pos._node;
Node* prev = cur->_prev;
// new Node时直接把参数转发给T的构造函数,在堆上原地构造
Node* newnode = new Node(std::forward<Args>(args)...);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
};来看看对比——同样插入一个 jys::string:
int main() {
list<jys::string> lt;
jys::string s1("111111111111");
jys::string s2("222222222222");
// 场景1:传左值
lt.push_back(s1); // const T& → 调string的拷贝构造
lt.emplace_back(s1); // 同样传左值 → 也调拷贝构造,和push_back没区别
// 场景2:传右值
lt.push_back(move(s2)); // T&& → 调string的移动构造
lt.emplace_back(move(s1)); // 同样传右值 → 也调移动构造,和push_back没区别
// 场景3:直接传构造参数——这是emplace_back真正的舞台!
lt.emplace_back("111111111111"); // 直接把const char*传给节点的string构造
// 在节点内部原地构造,一次构造搞定!
cout << "***********************" << endl;
lt.push_back("111111111111"); // 先用const char*构造临时string
// 再把临时string移动到节点中
// 两步操作:构造+移动(有可能被优化掉)
return 0;
}emplace_back 真正的优势在场景3:当你可以直接传递构造函数的参数时,emplace_back 避免了"先构造临时对象再拷贝/移动"的额外开销。
2.3 pair 的场景对比——差异非常明显
对 pair 这种复合类型,emplace_back 的威力更加直观:
int main() {
jys::list<pair<jys::string, int>> lt1;
// ===== push_back 方式 =====
pair<jys::string, int> kv("苹果", 1);
lt1.push_back(kv); // pair的拷贝构造 → 内部string的拷贝构造
cout << "***********************" << endl;
lt1.push_back(move(kv)); // pair的移动构造 → 内部string的移动构造
cout << "***********************" << endl;
lt1.push_back({"苹果", 1}); // 先构造临时pair → 再移动到节点
cout << "***********************" << endl;
// ===== emplace_back 方式 =====
// 直接传两个参数给pair的构造函数,在节点内部原地构造!
lt1.emplace_back("苹果", 1);
// 注意:不能写成 emplace_back({"苹果", 1}),因为{}不会被推导为pair
cout << "***********************" << endl;
return 0;
}关键区别:
| 方法 | 对 pair<string,int> 的操作 | 开销 |
|---|---|---|
| push_back(kv) | 拷贝构造pair → 内部string拷贝 | 深拷贝,开销最大 |
| push_back(move(kv)) | 移动构造pair → 内部string移动 | 较轻,但仍有移动 |
| push_back({"苹果", 1}) | 隐式构造临时pair + 移动 | 构造一次 + 移动一次 |
| emplace_back("苹果", 1) | 直接原地构造pair | 构造一次,没有额外开销 |
总结:当你需要往容器里加新元素且可以直接给出构造参数时,用 emplace_back;当你已经有一个现成的对象时,emplace_back 和 push_back 没有本质区别。
三、lambda 表达式:把函数写成一行的艺术
3.1 从仿函数的痛苦说起
在 lambda 出现之前,C++98 里要实现一个简单的比较逻辑,你得这么写:
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}
};
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
return 0;
}每换一个排序维度(价格↔评价)、每换一个方向(升序↔降序),都要新写一个仿函数类。放在代码里占位置不说,逻辑还和调用点离得老远。
3.2 lambda :一行代码,所见即所得
C++11 的 lambda 表达式就是为这种场景而生的:
int main() {
vector<Goods> v = {
{"苹果", 2.1, 5}, {"香蕉", 3, 4},
{"橙子", 2.2, 3}, {"菠萝", 1.5, 4}
};
// 价格升序——比较逻辑就写在调用处!
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;
});
return 0;
}优点一目了然:定义即使用,不需要起名字,不需要跳到别的地方看代码。这就是 lambda 的表达力。
3.3 lambda 语法详解
lambda 表达式的完整语法如下:
[捕获列表] (参数列表) mutable -> 返回值类型 { 函数体 }
四个组成部分:
| 部分 | 说明 | 可否省略 |
|---|---|---|
[捕获列表] | 捕获外部变量 | 不可省略(可以为空 []) |
(参数列表) | 形参列表 | 可以省略(无参时) |
mutable | 允许修改值捕获的变量(修改的是副本) | 可以省略 |
-> 返回值类型 | 返回类型 | 可以省略(编译器自动推导) |
{ 函数体 } | 函数体 | 不可省略 |
最精简的 lambda:
auto func1 = [] {
cout << "hello jys" << endl;
return 0;
};
func1(); // 调用注意:这里 [] 是捕获列表(空的),{} 是函数体——其他全省略了。
带参数和返回值的 lambda:
auto add1 = [](int x, int y) -> int { return x + y; };
cout << add1(1, 2) << endl; // 输出: 3
// -> int 可以省略,编译器从 return 语句自动推导返回值类型
auto add2 = [](int x, int y) { return x + y; };
cout << add2(3, 4) << endl; // 输出: 73.4 捕获列表——lambda 最精妙的设计
lambda 的重点和难点都在于捕获列表。它决定了 lambda 内部可以访问哪些外部变量。
值捕获[变量名]
捕获外部变量的副本,lambda 内部的修改不影响外部变量:
int a = 0, b = 1, c = 2;
auto func = [a, b](int x) mutable {
a++; // 修改的是a的副本,外部a不变
b++; // 修改的是b的副本,外部b不变
return a + b + x;
};
cout << func(1) << endl; // 内部 a=1, b=2, x=1 → 输出4
cout << "外部 a=" << a << endl; // 输出:外部 a=0 (没变!)注意:
mutable关键字是必须的,否则值捕获的变量在 lambda 内是 const 的,不能修改- 值捕获发生在 lambda 定义时,而不是调用时
引用捕获[&变量名]
捕获外部变量的引用,lambda 内部的修改直接影响外部变量:
int a = 0, b = 1;
auto func = [&a, &b] {
a++; // 修改的就是外部a
b++; // 修改的就是外部b
};
func();
cout << "a=" << a << ", b=" << b << endl; // 输出:a=1, b=2隐式捕获[=]和[&]
如果 lambda 里要用很多外部变量,一个个写捕获列表太麻烦:
int a = 0, b = 1, c = 2, d = 3;
// [=] 以值的方式捕获所有用到的外部变量
auto func1 = [=] {
return a + b + c; // a, b, c 都以值方式捕获
};
// [&] 以引用的方式捕获所有用到的外部变量
auto func2 = [&] {
a++; c++; // a, c 都以引用方式捕获,修改直接影响外部
};
func2();
cout << "a=" << a << ", c=" << c << endl; // 输出:a=1, c=3混合捕获[=, &变量]和[&, 变量]
还可以混合使用,实现"大部分用一种方式,个别变量例外":
int a = 0, b = 1, c = 2, d = 3;
// 大部分值捕获,但 c 和 d 用引用捕获
auto func4 = [=, &c, &d] {
// a++; b++; // ❌ 值捕获,不能修改(除非加mutable)
c++; // ✓ 引用捕获,可以修改
d++; // ✓ 引用捕获,可以修改
return a + b + c + d;
};
func4();
cout << a << " " << b << " " << c << " " << d << endl;
// 输出:0 1 3 4(c和d被修改了)
// 大部分引用捕获,但 a 和 b 用值捕获
auto func5 = [&, a, b] {
// a++; // ❌ 值捕获,不能修改
c++; // ✓ 引用捕获
d++; // ✓ 引用捕获
};全局变量:不需要捕获
全局变量在 lambda 中可以直接访问,不用捕获:
int y = 0; // 全局变量
auto func = [] {
y++; // 直接访问,不需要捕获
};3.5 lambda 的本质——编译器帮你写的仿函数
你有没有想过,lambda 到底是个什么类型?
auto func = [](int x, int y) { return x + y; };
cout << typeid(func).name() << endl; // 输出一串编译器生成的类名
lambda 的本质是编译器自动生成的匿名仿函数类。编译器看到了你的 lambda 表达式,就默默帮你生成了一个结构类似下面的类:
// 你看不到这个类,但编译器在背后就是这么干的
class __lambda_unique_id {
public:
// []的捕获内容对应这里的成员变量
// ()的参数列表对应 operator() 的形参
// {}的函数体对应 operator() 的函数体
int operator()(int x, int y) const {
return x + y;
}
};再看一个有捕获的 lambda 和对应的仿函数对比:
double rate = 0.49;
// lambda 版本
auto r2 = [rate](double money, int year) {
return money * rate * year;
};
// 等价的仿函数版本
class Rate {
public:
Rate(double rate) : _rate(rate) {}
double operator()(double money, int year) {
return money * _rate * year;
}
private:
double _rate;
};
Rate r1(rate);
// 两者用法完全一样
cout << r1(10000, 2) << endl; // 仿函数
cout << r2(10000, 2) << endl; // lambdarate 在 lambda 中对应值捕获,在仿函数中对应成员变量 _rate——两者在底层是完全等价的操作。
四、新的类功能
C++11 新增了两个默认成员函数:移动构造函数和移动赋值运算符重载。不过,想让它们自动生成的条件十分严苛:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一,那么编译器会自动生成一个默认移动赋值。
另外,如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
C++11 之前,如果你定义了一个构造函数,编译器就不再生成默认构造函数。有些时候你明明想让编译器再帮你生成,却没法说——直到 = default 的出现:
#include <string>
#include <iostream>
using namespace std;
namespace jys {
class string {
// ... (自定义string类的实现)
};
}
class Person {
public:
Person(const char* name = "张三", int age = 1)
: _name(name), _age(age) {}
// 我自定义了拷贝构造和移动构造,但还想让编译器生成默认版本
Person(Person&& p) = default; // 编译器,帮我生成默认移动构造
//Person(const Person& p) = delete;限制默认函数的生成
// 析构函数也用默认的
~Person() = default;
private:
jys::string _name;
int _age;
};
int main() {
Person s1;
Person s2 = s1; // 调用默认拷贝构造(逐成员拷贝)
Person s3 = std::move(s1); // 调用默认移动构造(逐成员移动)
return 0;
}= default 只能在类内部声明时使用,作用是显式要求编译器生成该函数的默认版本。另外,如以上代码所示,如果想要抑制默认函数生成,在函数声明后面加上=delete即可。
五、std::function——统一的可调用对象包装器
5.1 可调用对象
C++ 里有好几种"可以被调用"的东西:
- 普通函数
- 仿函数(重载了
operator()的类对象) - lambda 表达式
- 成员函数(需要通过对象或指针调用)
每种东西的类型都不一样,没法用一种统一的类型来声明和传递。比如你想写一个函数,接受"任意的可调用对象"作为参数——在 C++98 里只能通过模板,没法用统一的类型。
5.2function:一碗水端平
std::function 是一个类模板,可以把各种可调用对象包装成同一种类型。它定义在 <functional> 头文件中。
#include <functional>
#include <iostream>
#include <map>
#include <string>
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)> 表示 "接受两个int,返回int"的可调用对象
function<int(int, int)> f1 = f; // 包装普通函数
function<int(int, int)> f2 = Functor(); // 包装仿函数
function<int(int, int)> f3 = [](int a, int b) { // 包装lambda
return a + b;
};
cout << f1(1, 1) << endl; // 2
cout << f2(2, 2) << endl; // 4
cout << f3(3, 3) << endl; // 6
return 0;
}看看是不是很神奇?三种来源完全不同的东西,包装之后得到的 f1、f2、f3 的类型完全一样——都是 function<int(int, int)>。
5.3 包装成员函数——多了一点"绑定"的味道
成员函数和非成员函数不同,它们有一个隐藏的 this 指针参数。所以用 function 包装成员函数时,需要在类型签名里多一个参数:
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; // 2
// 普通成员函数 → 第一个参数是调用它的对象(用指针)
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus pl;
cout << f5(&pl, 1.1, 1.1) << endl; // 通过指针调用
// 也可以用对象值传递
function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(pl, 1.1, 1.1) << endl; // pl以值方式传入
cout << f6(Plus(), 1.1, 1.1) << endl; // 临时对象也可以
// 甚至可以用右值引用
function<double(Plus&&, double, double)> f7 = &Plus::plusd;
cout << f7(move(pl), 1.1, 1.1) << endl;
cout << f7(Plus(), 1.1, 1.1) << endl;
return 0;
}几点需要留意的地方:
&不能省:&Plus::plusi/&Plus::plusd获取成员函数地址时,&是必须的(这是语法要求,和获取函数指针时的&f不同,函数名的&可以省略但成员函数的不行)- 静态成员函数 = 普通函数:没有隐含的
this指针,所以包装签名和非成员函数完全一样 - 成员函数的第一个参数是
this:需要显式在function的签名中声明,类型可以是类名*、类名或类名&&
5.4map + function:实现策略模式
function 的一个经典应用场景是把函数映射表(策略模式的一种实现):
int main() {
// 字符串 → 操作函数的映射表
map<string, function<int(int, int)>> opFuncMap = {
{"+", [](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; }},
{"&", [](int x, int y) { return x & y; }},
{"|", [](int x, int y) { return x | y; }},
{"^", [](int x, int y) { return x ^ y; }}
};
// 使用起来极其方便
cout << opFuncMap["+"](3, 5) << endl; // 8
cout << opFuncMap["*"](3, 5) << endl; // 15
cout << opFuncMap["^"](6, 3) << endl; // 5
return 0;
}这种写法的好处:
- 增加新操作只需在 map 中添加一行,不需要修改任何其他代码
- 调用方完全不知道底层是函数指针、仿函数还是 lambda——全部统一为
function<int(int, int)> - 比
switch-case或一大串if-else优雅得多
六、std::bind——随心所欲绑定参数
6.1 为什么要 bind?
function 解决了"统一类型"的问题,但还有一个常见需求没解决:我有一组参数,想固定其中几个,只让调用者传剩下的。
int Sub(int a, int b) {
return (a - b) * 10;
}
假设我想把 Sub 变成"固定被减数为 100 的减法函数",现在调用者只需要传减数 b。没有 bind 时,你只能再包一层:
// 老办法:手动包装
auto sub100 = [](int b) {
return Sub(100, b);
};
cout << sub100(5) << endl; // (100 - 5) * 10 = 950
std::bind 让这个操作变得异常简单:
#include <functional> using namespace std; using placeholders::_1; using placeholders::_2; using placeholders::_3; // 一行搞定:把 Sub 的第一个参数固定为 100 auto sub100 = bind(Sub, 100, _1); cout << sub100(5) << endl; // (100 - 5) * 10 = 950
6.2 占位符:_1,_2,_3…
std::bind 的核心是占位符(placeholder)机制:
_1:代表"绑定后的新函数的第1个参数"_2:代表"绑定后的新函数的第2个参数"_3:代表"绑定后的新函数的第3个参数"
占位符定义在 std::placeholders 命名空间中:
using placeholders::_1; using placeholders::_2; using placeholders::_3; // ... 需要几个就 using 几个
6.3 bind 的四大经典用法
用法一:固定参数
int Sub(int a, int b) { return (a - b) * 10; }
// 固定第一个参数为100
auto sub1 = bind(Sub, 100, _1);
cout << sub1(5) << endl; // Sub(100, 5) = 950
// 固定第二个参数为100
auto sub2 = bind(Sub, _1, 100);
cout << sub2(5) << endl; // Sub(5, 100) = -950用法二:调整参数顺序
// 原本是 Sub(a, b),现在让调用者的第一个参数传给b,第二个传给a auto sub3 = bind(Sub, _2, _1); cout << sub3(10, 5) << endl; // Sub(5, 10) = -50 (参数被交换了!) cout << Sub(10, 5) << endl; // Sub(10, 5) = 50 (原始顺序)
用法三:多参数函数的灵活绑定
int SubX(int a, int b, int c) {
return (a - b - c) * 10;
}
// 分别固定第1、第2、第3个参数为100
auto sub5 = bind(SubX, 100, _1, _2); // 固定a=100,b和c由调用者提供
auto sub6 = bind(SubX, _1, 100, _2); // 固定b=100,a和c由调用者提供
auto sub7 = bind(SubX, _1, _2, 100); // 固定c=100,a和b由调用者提供
cout << sub5(5, 1) << endl; // SubX(100, 5, 1) = 940
cout << sub6(5, 1) << endl; // SubX(5, 100, 1) = -960
cout << sub7(5, 1) << endl; // SubX(5, 1, 100) = -960用法四:绑定成员函数——让调用者和普通函数一样自然
回忆一下,用 function 包装成员函数时,每次调用都需要显式传入对象。用 bind 可以把对象也一起绑定进去:
class Plus {
public:
Plus(int n = 10) : _n(n) {}
double plusd(double a, double b) {
return (a + b) * _n;
}
private:
int _n;
};
int main() {
// function方式:对象需要每次传入
function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(Plus(), 1.1, 1.1) << endl;
// bind方式:对象一次性绑定,后续调用和普通函数完全一样
function<double(double, double)> f8 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f8(1.1, 1.1) << endl; // 不需要再传对象!
return 0;
}对比很明显:
function<double(Plus, double, double)>——三个参数,每次要传对象function<double(double, double)>——两个参数,对象已经"内置"了
6.4 实战:用 bind 打造利率计算工具
最后来看一个实际场景。假设我们有一个复利计算的核心函数:
// 复利计算:返回利息金额
// rate: 年利率,money: 本金,year: 年数
auto calcInterest = [](double rate, double money, int year) -> double {
double ret = money;
for (int i = 0; i < year; i++) {
ret += ret * rate;
}
return ret - money;
};
cout << calcInterest(0.05, 10000000, 30) << endl; // 5%年利率,1000万,30年现在银行有很多种理财产品,利率不同、期限不同。直接调用 calcInterest 每次都要填三个参数,容易出错。用 bind 可以批量生成不同产品的计算函数:
// 1.5%年利率系列 function<double(double)> func3_1_5 = bind(calcInterest, 0.015, _1, 3); function<double(double)> func5_1_5 = bind(calcInterest, 0.015, _1, 5); function<double(double)> func10_1_5 = bind(calcInterest, 0.015, _1, 10); // 2.5%年利率系列 function<double(double)> func3_2_5 = bind(calcInterest, 0.025, _1, 3); function<double(double)> func5_2_5 = bind(calcInterest, 0.025, _1, 5); function<double(double)> func10_2_5 = bind(calcInterest, 0.025, _1, 10); // 使用时只需传入本金 cout << "1.5% 存3年,100万 → " << func3_1_5(1000000) << endl; cout << "1.5% 存5年,100万 → " << func5_1_5(1000000) << endl; cout << "1.5% 存10年,100万 → " << func10_1_5(1000000) << endl; cout << "2.5% 存3年,100万 → " << func3_2_5(1000000) << endl; cout << "2.5% 存5年,100万 → " << func5_2_5(1000000) << endl; cout << "2.5% 存10年,100万 → " << func10_2_5(1000000) << endl;
用 bind 提前固化利率和年限之后,得到的 function<double(double)> 只接受本金一个参数。调用方不需要关心利率、年限这些细节,完全由上下文保证正确性。这在金融系统中非常实用——可以封装成一个个理财产品对象,避免参数传递中的错误。
七、lambda、function、bind 三者的关系
学到这里,你可能会有个疑问:这三个东西好像能做的事情有重叠?是的,它们之间有明确的定位:
lambda → 快速定义一个"匿名函数",就地使用 function → 用一个"统一的类型"来保存和传递各种可调用对象 bind → 把已有的可调用对象的某些参数"固定"住,生成签名单一的新对象
一个典型的配合使用场景:
// 1. 用 lambda 定义核心逻辑
auto calc = [](double rate, double money, int year) -> double {
double ret = money;
for (int i = 0; i < year; i++) ret += ret * rate;
return ret - money;
};
// 2. 用 bind 固定部分参数
auto fiveYearFund = bind(calc, 0.035, _1, 5);
// 3. 用 function 统一存储
map<string, function<double(double)>> products;
products["稳健型5年"] = fiveYearFund;
products["进取型5年"] = bind(calc, 0.06, _1, 5);
// 4. 调用
cout << products["稳健型5年"](100000) << endl;八、总结
本文从可变参数模板开始,串联了 C++11 的一些特性:
可变参数模板 (Args...)
↓
参数包展开的两种方式(递归展开 / 逗号表达式展开)
↓
可变参数 + 完美转发 → emplace_back 原地构造
↓
lambda 表达式 → 捕获列表 / 值 vs 引用 / 隐式捕获
↓
std::function → 统一各种可调用对象的类型
↓
std::bind → 固定参数、调整顺序、简化调用
这些特性之间是紧密关联的。理解它们之间的"化学反应"比孤立地背语法重要得多:
- 可变参数模板 + 完美转发 = emplace_back,让容器的插入性能达到极致
- lambda + function = 统一的回调机制,杜绝了函数指针、仿函数各写一套的乱象
- function + bind = 完备的函数式工具链,让你能像搭积木一样构造复杂行为
和上一篇文章加起来,C++11 的核心体系你已经走完了:统一初始化 → 右值引用 → 移动语义 → 万能引用 → 完美转发 → 可变参数模板 → lambda → function → bind。这套知识体系是现代 C++ 程序员的"必修课",掌握了它们,你在阅读和编写现代 C++ 代码时会如鱼得水。
到此这篇关于C++11:从可变参数模板到bind绑定器的文章就介绍到这了,更多相关C++ bind绑定器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
