【C++11篇(四)】新的类功能、lambda 与包装器详解


大家好,欢迎来到 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 defaultdelete:精确控制默认函数)
      • [1.5 `final` 与 `override` (简要回顾)](#1.5 finaloverride (简要回顾))
    • [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 定制利率计算器)
    • 结语:

1. 新的类功能:默认成员函数的演化

1.1 回顾 C++98 的 6 个默认成员函数

  在传统 C++ 中,如果我们定义一个类而不写某些特殊函数,编译器会在需要时自动生成它们。经典的说法是 "6 个默认成员函数"

  1. 默认构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值运算符重载
  5. 取地址运算符重载
  6. const 取地址运算符重载

  实际开发中,真正重要的就是前四个。后两个默认取地址函数基本不会被刻意重载,所以大多数时候我们只关心构造、析构、拷贝构造和拷贝赋值。

  C++11 在此基础上引入了移动语义,因此编译器也学会了自动生成与移动相关的函数。

1.2 C++11 新增的默认移动构造和移动赋值

从 C++11 开始,类的默认成员函数变成了 8 个,新增的两个是:

  • 移动构造函数
  • 移动赋值运算符重载

  但注意:编译器并不会无脑地总是自动生成它们,它的生成条件相当讲究。规则如下:

  如果你没有自己实现移动构造函数,而且也没有自己实现析构函数、拷贝构造函数、拷贝赋值重载中的任意一个 ,编译器就会自动生成一个默认的移动构造函数

  同样的规则也适用于移动赋值:

  如果你没有自己实现移动赋值函数,而且也没有自己实现析构函数、拷贝构造函数、拷贝赋值重载中的任意一个 ,编译器就会自动生成一个默认的移动赋值运算符

  换句通俗的话讲:一旦你手动干预了"资源管理"相关的那几个函数(析构、拷贝构造、拷贝赋值),编译器就认为你对资源的掌控有特殊需求,因此它不再敢擅自帮你生成移动操作,以免破坏你的意图。 反之,如果你完全没碰那些函数,编译器就放心地默认生成移动构造和移动赋值。

那么,默认生成的移动构造和移动赋值具体做些什么呢?

  • 对于内置类型 成员(如 int、double、指针),默认移动执行的是逐成员按字节拷贝(效果和拷贝一样,因为内置类型没有"移动"的概念)。
  • 对于自定义类型 成员,编译器会查看这个成员有没有实现移动构造/移动赋值。如果实现了,就调用它的移动构造/移动赋值;如果没有实现,则退一步调用它的拷贝构造/拷贝赋值。

  这样,最终整个对象的"移动"效果就是:能移动的成员就移动,不能移动的就拷贝。对于标准库里的 std::stringstd::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 defaultdelete:精确控制默认函数

  有时我们会面临一个两难的境地:因为某些特殊原因,我们不得不手动写一个拷贝构造函数(比如需要深拷贝),但这却导致编译器不再生成移动构造函数。如果我们确信移动构造的默认行为是没问题的,怎么办呢?

  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 finaloverride (简要回顾)

这两个关键字主要用于继承和多态场景,此处不再详细展开。

  • final:修饰类表示不可被继承 ,修饰虚函数表示不可在派生类中被重写
  • override:显式声明该函数重写了基类的虚函数,编译器会检查签名是否一致,避免因为参数不同而变成"新建了一个函数"。

如果忘了,可以看看博主之前写的文章。


2. STL 中的一些变化(概览)

C++11 给标准模板库也带来了大量新东西,主要包括:

  • 新容器unordered_mapunordered_setarrayforward_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 的默认规则是:函数体中只能使用其自身的参数、函数体内定义的变量、全局变量和静态局部变量,不能直接使用外层作用域的局部变量。如果想用,就得通过捕捉列表"捕获"进来。

捕捉方式有三种基本形态:

  1. 显式捕捉:手动列出要捕捉的变量,并指定传值还是传引用。
  2. 隐式捕捉 :用 =& 让编译器自动捕捉。
  3. 混合捕捉:隐式捕捉 + 部分变量特殊处理。
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]:除了 ab 是值捕捉,其余都是引用捕捉。
  • [=, &a, &b]:除了 ab 是引用捕捉,其余都是值捕捉。
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::functionstd::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 adouble 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 解决逆波兰表达式求值

传送门

  逆波兰表达式求值是栈的经典应用。传统写法往往使用 switchif-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 参数化固定部分参数生成新函数的手法,在函数式编程中非常常见,也让代码更容易复用。


结语:

  今天的内容到这里就结束了,希望你能有所收获~

干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _

相关推荐
2zcode1 小时前
基于MATLAB图像处理的啤酒瓶口缺陷检测系统设计与实现
开发语言·图像处理·matlab
进击的程序猿~1 小时前
Go Zero源码阅读2
开发语言·后端·golang
hdsoft_huge1 小时前
JDK系列19:JVM全维度调优实战,内存、元空间、线程栈、GC参数压测调优完整方案
java·开发语言·jvm
哥不想学算法9 小时前
【C++】字符串字面量拼接
开发语言·c++
gugucoding11 小时前
21. 【C语言】打包不同类型:结构体
c语言·开发语言
Brookty11 小时前
【JavaEE】线程安全(一).4:写块串行保安全、CAS
java·开发语言·java-ee·多线程·线程安全
F202269748612 小时前
西门子 PLC 与 C# 通信
开发语言·c#
gugucoding12 小时前
31. 【C语言】堆栈与队列的实现
c语言·开发语言·数据结构·链表