C++11:从可变参数模板到bind绑定器

C++11绑定器bind及function机制 之前在学muduo网络库时,看到陈硕以基于对象编程的方式,大量使用boost库中的bind和function机制,如今,这些概念都已引入至C++11,包含在头文件中。本篇文章主要梳理C++绑定器相关的内容以及C++11中引入的functionbind1st、bind2nd、bind(C++11)。学完本篇内容,将对C++绑定器及function机制等的底层实现有深刻理解,那么我们开始说吧。本文通过C++11之前的bind1st和bind2nd。 阅读详情

目录


前言:书接上回

在上一篇文章《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.3 sizeof...(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;  // 输出: 7

3.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;  // lambda

rate 在 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++ 里有好几种"可以被调用"的东西:

  1. 普通函数
  2. 仿函数(重载了 operator() 的类对象)
  3. lambda 表达式
  4. 成员函数(需要通过对象或指针调用)

每种东西的类型都不一样,没法用一种统一的类型来声明和传递。比如你想写一个函数,接受"任意的可调用对象"作为参数——在 C++98 里只能通过模板,没法用统一的类型。

5.2 function:一碗水端平

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;
}

几点需要留意的地方:

  1. & 不能省:&Plus::plusi / &Plus::plusd 获取成员函数地址时,& 是必须的(这是语法要求,和获取函数指针时的 &f 不同,函数名的 & 可以省略但成员函数的不行)
  2. 静态成员函数 = 普通函数:没有隐含的 this 指针,所以包装签名和非成员函数完全一样
  3. 成员函数的第一个参数是 this:需要显式在 function 的签名中声明,类型可以是 类名*、类名 或 类名&&

5.4 map + 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绑定器和function函数对象 施磊C++语言高级进阶课程,主要讲解绑定器、function函数对象、lambda表达式 阅读详情

相关推荐

AutoLISP 学习 (一)

AutoLISP基本语法结构、语法 以括号组成表达式,左右括号一定要成对,单、双引号也要成对,例如: (defun c:hello() (princ "\nHello world!") (prin1) ) 表达式格式:(函数名 操作数 操作数 …),例如: (setq p1 (list 0 0)) (setq p2 (list 10 10)) (setq p3 (list 20 20)) ;;; CAD中画直线,经过p1 p2 p3 (command "LINE" p1 p2 p3 ""

m0_54575622的博客 2120

深入理解C++11新特性:从移动语义到多线程编程

C++11 是现代 C++ 发展的重要里程碑,引入了多项革命性特性。本文系统性地介绍了五大核心改进:1)**移动语义与类功能增强**,通过移动构造函数和移动赋值运算符提升性能;2)**可变参数模板**,支持任意数量模板参数,为 STL 容器的 `emplace` 接口提供基础;3)**Lambda 表达式**,简化匿名函数定义,替代复杂函数对象;4)**包装器**,包括 `std::function` 统一可调用对象和 `std::bind` 参数绑定;5)**标准线程库**,提供跨平台多线程支持及原子操作

2303_77004089的博客 383

搞定安卓11+抓包难题:用Kitsune Mask、magic_overlayfs和LSPosed三件套绕过SSL Pinning(保姆级教程)

本文详细解析了安卓11+系统中抓包面临的核心挑战,包括SSL Pinning和Root检测机制,并提供了使用Kitsune Mask、magic_overlayfs和LSPosed三件套的完整解决方案。通过动态分区技术解析和工具链配置,帮助开发者绕过安全限制,实现高效抓包分析。

weixin_42665255的博客 440

C++学习/复习30--{}统一列表初始化/decltype/右值引用与移动语义/perfectforward完美转发/lambda/可变参数模板/emplace/包装器function与bind

C++11引入统一初始化{},支持几乎所有类型,优先匹配initializer_list构造函数,提升类型安全与初始化简洁性。auto与decltype实现类型推导,nullptr替代NULL避免歧义。范围for简化遍历,std::move与移动语义避免深拷贝,emplace系列实现原地构造,提升性能。Lambda表达式支持捕获、完美转发,可变模板参数与折叠表达式增强泛型编程能力。std::function与bind提供通用回调包装,全面推动现代C++高效、安全开发。

Hncj2022的博客 218

立创开源Link Hub多功能USB扩展坞:CH339F主控+4层板设计,实测网口/存储性能

本文深度评测了立创开源Link Hub多功能USB扩展坞,其采用CH339F主控芯片与4层PCB板设计,并严格进行阻抗匹配。实测表明,该扩展坞的USB接口与microSD读卡器性能均接近USB 2.0理论极限,百兆以太网口转换效率高,性能损失极小。设计上注重电源保护与信号完整性,是一个兼顾稳定性与成本的高完成度开源硬件项目。

weixin_36173034的博客 154

SoIC、CoWoS、InFO一次讲透:从2D/2.5D/3D看懂先进封装

先进封装是后摩尔时代提升芯片性能的关键路径,但面对SoIC、CoWoS、InFO这些名词,很多人容易混淆。理解它们,需从最基本的2D、2.5D、3D封装概念入手:2D是传统平房,2.5D通过硅中介层实现高密度互连,3D则让芯片直接堆叠。CoWoS作为2.5D中介层封装,已成为AI芯片与HBM高带宽存储集成的行业标准;SoIC通过铜-铜混合键合实现真正的3D堆叠,为逻辑与缓存提供极致带宽;InFO以扇出型封装兼顾成本与集成度,广泛用于移动SoC。在系统级封装(SiP)和CPO共封装光学等前沿趋势中,这些技术往

weixin_42388898的博客 309

【C++】:bind绑定器和function函数对象机制

这里,我创建了一个返回值为int,参数有两个,从左往右为int,int类型的函数指针。这些可调用对象的调用形式相同,甚至拥有相同的功能,但是却由于类型千差万别无法统一处理。例如,我们想要统一调用以上可调用对象,使用‘+’来调用add函数,而使用‘-’调用sub匿名函数…C++常见可调用对象有:函数、指针、匿名函数(lambda表达式)、函数对象(重载了函数调用运算符的类)以及使用bind创建的对象。如上,我们系统的介绍了function的使用方法和应用,但是它究竟是如何实现的呢?的基本用法有了初步的了解。

2301_79181030的博客 1242

100天精通Python(爬虫篇)——第122天:基于selenium接管已启动的浏览器(反反爬策略)

100天精通Python(爬虫篇)——第122天:基于selenium接管已启动的浏览器(反反爬策略)

努力让自己发光,对的人才能迎着光而来 13万+

C++11:包装器(适配器模式)

function包装器也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。 那么function包装器究竟是为了解决什么问题呢??C++11设置出来的初衷是什么呢??

weixin_51142926的博客 3074

coze 怎么读取excel文件,使用循环,数据库完成LLM问答并存储

本文介绍了如何在Coze平台实现"Excel读取+循环处理+LLM问答+数据库存储"的工作流。主要分为三个步骤:1)使用代码节点解析Excel文件为问答对数组;2)通过循环节点遍历问答对,调用LLM节点生成回答;3)利用数据库节点将问答结果存储到Coze内置数据库中。文中提供了代码示例和节点配置说明,并强调了注意事项如Excel格式要求、LLM调用频率控制等。该工作流适用于批量构建知识库问答系统并持久化存储结果。

ZhangJiqun&191cm&85kg 1780

【YOLO系列】YOLOv8详解:模型结构、损失函数、训练方法及代码实现

YOLOv8是Ultralytics团队2023年推出的高效目标检测框架,在速度与精度平衡、多任务支持和易用性方面实现突破。模型采用改进的CSPDarknet主干网络、C2f模块和SPPF加速结构,结合PAN-FPN特征融合和解耦检测头设计,支持目标检测、实例分割和关键点检测任务。创新性地使用CIoU定位损失、Focal Loss置信度损失和动态样本分配策略,显著提升检测性能。实验表明,YOLOv8较前代模型mAP提升5-10%,在TensorRT加速下可达1000FPS,提供5种尺寸灵活选择,成为实时目标

一碗白开水一 2778

【C++】C++11中的包装器和绑定器

本篇文章围绕C++11新增的function包装器和bind绑定器展开讲解,包含其概念、可调用对象和应用场景等内容

Eristic0618的博客 1512

[AI工具评测] 为什么功能全面的 Kimi Claw,最终还是被放弃?

《KimiClaw:功能整合与体验落差的AI工具实践》 KimiClaw作为集LLM、Agent、云端执行和SaaS分发于一体的AI工具,理论上具备个人开发者所需的全栈能力:原生整合大模型与Agent、支持云端Docker部署、可生成Web应用并通过飞书协作。然而实际高频使用中暴露三大核心问题:响应延迟导致思考节奏断裂(非流式输出)、云端黑箱执行缺乏可控性(无法实时干预)、系统稳定性不足(网关和隧道偶发故障)。这些体验落差表明,功能完整性不等于使用高效性——对开发者而言,实时响应、执行透明度和系统可靠性比自

技术小甜甜:传播可落地技术,帮助个人和企业降低解决真实场景问题的成本 531

【C++11】三大神器之——包装器和绑定器

如果你还不知道 C++ 11 引入的包装器和绑定器是什么,可以读读这篇文章,看看有什么 启发;如果你已经对包装器和绑定器了如指掌,也可以读读这篇文章,看看有什么 补充。欢迎交流~

ZER00000001的博客 1663

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

【C++11】闭包:仿函数operator() && 绑定器bind && 包装器function && lambda表达式

​闭包有很多种定义, 一种说法是, 闭包是带有上下文的函数。说白了, 就是有状态的函数,就是有自己的变量。更直接一些,就是一个类换了个名字而已。​一个函数, 带上了一个状态, 就变成了闭包了。那什么叫“带上状态”呢?意思是这个闭包有属于自己的变量, 这些个变量的值是创建闭包的时候设置的, 并在调用闭包的时候, 可以访问这些变量。​函数是代码, 状态是一组变量, 将代码和一组变量捆绑 (bind) , 就形成了闭包。闭包的状态捆绑, 必须发生在运行时。

lirendada的博客 1501

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

C++11中bind绑定器和function函数对象

C++11中引入bind绑定器和function函数对象 一. bind1st和bind2nd 二. 模板的完全特例化和非完全特例化 三. function函数对象 四. bind和function实现线程池 五. lambda表达式

qq_41721746的博客 1311

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

bind绑定器和function函数对象

熟悉函数对象和绑定器的应用,并了解它们底层的原理。

m0_53441395的博客 860

【C++】C++11 包装器function 与 绑定器 bind

C++11包装器function 与 绑定器 bind

weixin_61091020的博客 1万+

117-C++11中引入的bind绑定器和function函数对象

1、C++11中引入的bind绑定器和function函数对象 1.1、bind1st和bind2d使用 bind1st和bind2d是STL中的; bind1st和bind2d作用: 将二元函数对象的一个参数绑定,使其变为一元函数对象; 缺点: 只能用于二元函数对象。 函数对象: 对象拥有小括号重载函数的对象。 #include <functional> --包含c++库中的所有的函数对象 #include <algorithm> --包含了c++库中的所有

Edward_LF的博客 906

C++11 学习笔记 std::function和bind绑定器

一.std::function       C++中的可调用对象虽然具有比较统一操作形式(除了类成员指针之外,都是后面加括号进行调用),但定义方法五花八门。为了统一泛化函数对象,函数指针,引用函数,成员函数的指针的各种操作,让我们可以按更统一的方式写出更加泛化的代码,C++11推出了std::function。   std::function是可调用对象的包装器。它是一个类模板,可以容

易水的博客 4518

详解bind绑定器和function函数对象机制

bind1st和bind2nd使用代码举例如下:自己动手实现bind1st和find_if:bind和function机制; 模板的完全特例化(全特化)和部分特例化(偏特化);区分一下函数类型和函数指针类型; 通过vector演示模板特例化的使用;自己动手实现function 理解原理;bind绑定器使用举例;muduo源码中的线程池用C++代码模拟;lambda表达式的原理和实践 ;通过lambda表达式实现有自定义优先级队列的比较方式;

C++开发 2037

C++11新增特性—>可变参数模板、lambda表达式、包装器

1、【概念】可变参数模板是C++11新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接受可变参数的函数模板和类模板,这里主要针对函数模板的可变参数。在C++11之前,类模板和函数模板中只能包含固定数量的模板参数,可变模板参数无疑是一个巨大的改进,但由于可变参数模板比较抽象,因此使用起来需要一定的技巧。在C++11之前其实也有可变参数的概念,比如printf函数就能够接收任意多个参数,但这是函数参数的可变参数,并不是模板的可变参数。

w8848w的博客 650

C++学习进阶:C++11特性(下)

可变参数模板是C++11引入的一种特性,它允许函数或类模板接受任意数量的参数。通过使用可变参数模板,我们可以编写更加灵活和通用的代码。可变参数模板使用...表示参数的可变性。我们可以在函数模板或类模板中使用可变参数模板。我们在命令行参数的学习中,知道参数是通过argv这个指针数组获取的,而对于可变参数模版它的参数是通过函数形参参数包中获取的。而Args对应着很多类型,我们在进行args访问时,会进入对应的...中不断“递归”查找到参数,并进行操作。这个场景也可以这样子解析参数包!

weixin_73234157的博客 1099

C++11 std::function和std::bind绑定器

前言 C++11增加了std::function和std::bind,使得使用标准库函数时变得方便,而且还能方便地实现延迟求值。C++中,存在“可调用对象”这么一个概念。准确来说,可调用对象有如下的定义: (1)是一个函数指针 (2)是一个具有operator()成员函数的类对象(仿函数) (3)是一个可被转换为函数指针的类对象 (4)是一个类成员(函数)指针 1. std::func

m_buddy的博客 3037

C++11中的bind绑定器和function函数对象

本文主要介绍C++11中的bind绑定器和function函数对象的概念和常见用法。

TechNomad的博客 585

浅谈C++绑定器bind1st、bind2nd和函数对象function

今天我们先来谈谈C++ 标准库里面的绑定器bind1stbind2nd和函数对象function。

icc_hhy的博客 1410
上一篇: C++11初探:C++发展史上的里程碑
下一篇: C++异常处理完全指南
jinyishu_
博客等级 码龄1年 53粉丝 82原创
评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值