【C++】C++11(右值引用和移动语义、可变参数模板 和 包装器)

文章目录

  • 一、列表初始化
    • [1. C++98传统的{ }](#1. C++98传统的{ })
    • [2. C++11中的{ }](#2. C++11中的{ })
    • [3. C++11中的 std::initializer_list](#3. C++11中的 std::initializer_list)
  • 二、右值引用
    • [1. 左值和右值](#1. 左值和右值)
    • [2. 左值引用和右值引用(讨论 右值引用变量 的属性)](#2. 左值引用和右值引用(讨论 右值引用变量 的属性))
    • [3. 引用延长生命周期](#3. 引用延长生命周期)
    • [4. 左值和右值的参数匹配](#4. 左值和右值的参数匹配)
  • 三、移动语义
    • [1. 左值引用的使用场景 及其 局限性(有些场景必须 传值返回)](#1. 左值引用的使用场景 及其 局限性(有些场景必须 传值返回))
    • [2. 移动构造 和 移动赋值(移动构造 与 拷贝构造 的对比)](#2. 移动构造 和 移动赋值(移动构造 与 拷贝构造 的对比))
    • [3. 右值引用 和 移动语义 解决传值返回问题](#3. 右值引用 和 移动语义 解决传值返回问题)
      • [3.1 右值对象构造场景](#3.1 右值对象构造场景)
      • [3.2 右值对象赋值场景](#3.2 右值对象赋值场景)
    • [4. 右值引用 和 移动语义在传参中的提效(STL容器的push和insert系列接口 增加右值引用版本)](#4. 右值引用 和 移动语义在传参中的提效(STL容器的push和insert系列接口 增加右值引用版本))
  • [四、左值引用 和 右值引用的补充知识](#四、左值引用 和 右值引用的补充知识)
    • [1. 类型分类(了解即可)](#1. 类型分类(了解即可))
    • [2. 引用折叠](#2. 引用折叠)
      • [2.1 引用折叠的介绍 和 规则(typedef中的类型操作 构成引用的引用 的场景)](#2.1 引用折叠的介绍 和 规则(typedef中的类型操作 构成引用的引用 的场景))
      • [2.2 函数模板 构成引用的引用 的场景(实现 万能引用)](#2.2 函数模板 构成引用的引用 的场景(实现 万能引用))
    • [3. 完美转发(搭配 万能引用 使用)](#3. 完美转发(搭配 万能引用 使用))
  • 五、可变参数模板
    • [1. 基本语法及原理](#1. 基本语法及原理)
    • [2. 包扩展](#2. 包扩展)
    • [3. empalce系列接口(empalce系列接口 比 insert和push系列接口高效)](#3. empalce系列接口(empalce系列接口 比 insert和push系列接口高效))
  • 六、新的类功能
    • [1. 默认的移动构造和移动赋值(先看 三.2)](#1. 默认的移动构造和移动赋值(先看 三.2))
    • [2. defult和delete](#2. defult和delete)
    • [3. final与override](#3. final与override)
  • 七、lambda
    • [1. lambda表达式语法](#1. lambda表达式语法)
    • [2. 捕捉列表](#2. 捕捉列表)
    • [3. lambda的原理](#3. lambda的原理)
    • [4. lambda的使用场景](#4. lambda的使用场景)
  • 八、包装器
    • [1. function](#1. function)
    • [2. bind](#2. bind)
  • 九、智能指针

一、列表初始化

1. C++98传统的{ }

C++98中,数组 和 结构体 可以用 { } 进行初始化:

cpp 复制代码
struct Point
{
    int _x;
    int _y;
};

int main()
{
    int array1[] = {1, 2, 3, 4, 5};
    int array2[5] = {0};
    Point p = {1, 2};
    return 0;
}

2. C++11中的{ }

  • C++11以后想一初始化方式,试图实现一切对象皆可用 { } 初始化,{ } 初始化也叫做 列表初始化
  • 内置类型支持,自定义类型也支持(自定义类型本质是类型转换,中间会产生临时对象,最后编译器优化了以后变成直接构造)
  • { } 初始化的过程中,可以省略掉 =
  • C++11列表初始化的本意是想实现⼀个统一的初始化方式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{ }初始化会很方便
cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

struct Point
{
    int _x;
    int _y;
};

class Date
{
public:
    Date(int year = 1, int month = 1, int day = 1)
        : _year(year), _month(month), _day(day)
    {
        cout << "Date(int year, int month, int day)" << endl;
    }
    
    Date(const Date &d)
        : _year(d._year), _month(d._month), _day(d._day)
    {
        cout << "Date(const Date& d)" << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    // 1.C++98支持的
    int a1[] = {1, 2, 3, 4, 5};
    int a2[5] = {0};
    Point p = {1, 2};

    // 2.C++11支持的
    // (1)内置类型支持
    int x1 = {2};

    // (2)自定义类型支持
    // 这里本质是用 { 2025, 1, 1 } 构造⼀个Date临时对象
    // 临时对象再去拷贝构造d1,编译器优化后合二为⼀变成 { 2025, 1, 1 } 直接构造初始化d1
    // 运行⼀下,我们可以验证上面的理论,发现是没调用拷贝构造的
    Date d1 = {2025, 1, 1};
    // 这里d2引用的是 { 2024, 7, 25 } 构造的临时对象(临时对象具有常属性,所以必须用const引用)
    const Date &d2 = {2024, 7, 25};
    // 特殊场景:需要注意的是 C++98支持单参数时类型转换,也可以不用 {}
    Date d3 = {2025};
    Date d4 = 2025;

    // 可以省略掉 =
    Point p1{1, 2};
    int x2{2};
    Date d5{2024, 7, 25};
    const Date &d6{2024, 7, 25};
    // 只有{}初始化,才能省略 =
    // 不支持 Date d4 = 2025; 省略 =,变为: Date d7 2025;(错误写法)
    
    vector<Date> v;
    Date d7 = {2002, 1, 5};
    v.push_back(d7);
    v.push_back(Date(2002, 1, 5));
    // 比起有名对象和匿名对象传参,这里{}更有性价比
    v.push_back({2025, 1, 1});
    return 0;
}

3. C++11中的 std::initializer_list

  • 上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如一个vector对象,我想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持, vector< int > v1 = {1,2,3};vector< int > v2 = {1,2,3,4,5};
  • C++11库中提出了一个 std::initializer_list 的类
bash 复制代码
(1) std::initializer_list<int> il = { 10, 20, 30 }; 

(2) auto il = { 10, 20, 30 }; 
// the type of il is an initializer_list
// 这两种写法的效果是一模一样的,使用 auto 能自动推导出 il 为 initializer_list类型

这个类的本质是底层创建一个数组,将数据拷贝过来 ,std::initializer_list 内部有两个指针分别指向数组的开始和结束。


// 这是他的文档:链接 initializer_list(std::initializer_list 支持迭代器遍历)

  • STL容器支持一个std::initializer_list的构造函数,也就支持任意多个值构成的 {x1, x2, x3...} 进行初始化。STL中的容器支持任意多个值构成的 {x1, x2, x3...} 进行初始化,就是通过 std::initializer_list的构造函数支持的。
cpp 复制代码
// 1.STL中的容器的构造函数 都增加了⼀个initializer_list版本
vector(initializer_list<value_type> il);
list(initializer_list<value_type> il);
map(initializer_list<value_type> il, const key_compare &comp = key_compare());
// ...

// 2.容器的赋值函数 也支持initializer_list版本
vector &operator=(initializer_list<value_type> il);
map &operator=(initializer_list<value_type> il);
// ...

// 模拟实现vector类中构造函数的initializer_list版本

cpp 复制代码
template <class T>
class vector
{
public:
    typedef T *iterator;
    
    // 模拟实现vector类中构造函数的initializer_list版本
    vector(initializer_list<T> il)
    {
        for (auto e : il)
            push_back(e);
    }
    // ...

private:
    iterator _start = nullptr;
    iterator _finish = nullptr;
    iterator _endofstorage = nullptr;
};

(1) 示例一: initializer_list对象 底层是在栈上创建一个数组

cpp 复制代码
#include <iostream>
using namespace std;

int main()
{
    initializer_list<int> mylist;
    mylist = {10, 20, 30};
    // 这⾥begin和end返回的值是 initializer_list对象中存的两个指针
    // 这两个指针的值跟i的地址跟接近,说明 initializer_list对象创建的数组 存在栈上
    int i = 0;
    cout << mylist.begin() << endl;
    cout << mylist.end() << endl;
    cout << &i << endl;

    return 0;
}


(2) 示例二:

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <map>
using namespace std;

int main()
{
    // 1. STL容器支持initializer_list版本的构造函数
    // {}列表中可以有任意多个值
    // 这两个写法语义上还是有差别的:
    // 第⼀个v1:{1, 2, 3, 4, 5} 直接构造 v1,
    // 第二个v2:{1, 2, 3, 4, 5} 构造临时对象 + 临时对象拷贝构造 v2; 编译器优化后,变为直接构造
    // 第三个v3 跟 v2写法的语义一致({} 初始化的过程中,可以省略掉 =)
    vector<int> v1({1, 2, 3, 4, 5});
    vector<int> v2 = {1, 2, 3, 4, 5};
    vector<int> v3{1, 2, 3, 4, 5};
    // 这里v4引用的是 {1, 2, 3, 4, 5} 构造的临时对象(临时对象具有常属性,所以必须用const引用)
    const vector<int> &v4 = {1, 2, 3, 4, 5};

    // 这里是pair对象的{}初始化 和 map的initializer_list构造结合到⼀起用了
    // {"sort", "排序"} 和 {"string", "字符串"} 构造两个 pair<string, string>对象
    // 两个pair<string, string>对象 构造map类临时对象 + 临时对象拷贝构造 dict; 编译器优化后,变为直接构造
    map<string, string> dict = {{"sort", "排序"}, {"string", "字符串"}};

    // 2. STL容器支持initializer_list版本的赋值重载
    v1 = {10, 20, 30, 40, 50};
    dict = {{"one", "第一"}, {"two", "第二"}};
    return 0;
}
  • 注意以下两种写法的区别:
    // 有编译器优化的情况下,两种写法最终的效果无区别
cpp 复制代码
(1)直接构造
vector<int> v1({1, 2, 3, 4, 5});

(2)构造 vector<int>类的 临时对象 + 临时对象拷贝构造 v2; 编译器优化后,变为直接构造
vector<int> v2{1, 2, 3, 4, 5};
// 省略了等号=,实际是:vector<int> v2 = {1, 2, 3, 4, 5};

二、右值引用

1. 左值和右值

  • 左值是一个表示数据的表达式(如变量名或解引用的指针) ,一般是有持久状态,存储在内存中,我们可以获取它的地址左值可以出现赋值符号的左边,也可以出现在赋值符号右边定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
  • 右值也是⼀个表示数据的表达式 ,要么是 字面值常量 、要么是表达式求值过程中创建的 临时对象匿名对象 等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边右值不能取地址
  • 值得一提的是,左值的英文简写为lvalue,右值的英文简写为rvalue。传统认为它们分别是left value、right value 的缩写。现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象;而 rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址 ,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说 左值和右值的核心区别就是能否取地址
cpp 复制代码
#include <iostream>
using namespace std;

int add(int x, int y)
{
    int z = x + y;
    return z;
}

int main()
{
    // 1.左值:可以取地址
    // 以下的p、b、c、*p、s、s[0]就是常见的左值
    int *p = new int(0);
    int b = 1;
    const int c = b;
    *p = 10;
    string s("111111");
    s[0] = 'x';

    cout << &c << endl;
    cout << (void *)&s[0] << endl;

    double x = 1.1, y = 2.2;
    // 2.右值:不能取地址
    // 以下几个10、x + y、add(x, y)、string("11111")都是常见的右值
    10;
    x + y; // 产生运算的表达式结果存储在临时对象
    add(x, y); // 值返回(除指针类型或引用类型返回外,都是值返回)的函数,它的返回值存储在临时对象
    string("11111"); // 匿名对象属于右值

    // 右值取地址会直接报错
    // cout << &10 << endl;
    // cout << &(x+y) << endl;
    // cout << &(add(x, y)) << endl;
    // cout << &string("11111") << endl;
    return 0;
}

补充(临时对象产生的情形):
(1)产生运算的表达式结果存储在临时对象
(2)在类型转换过程中会产生临时对象存储中间值


(3)除返回值类型为指针类型或引用类型之外,其它返回类型(void除外)在返回过程中都会创建临时对象

2. 左值引用和右值引用(讨论 右值引用变量 的属性)

  • Type& r1 = x;
    Type&& rr1 = y;
    第一个语句就是左值引用,左值引用就是给左值取别名 ;第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
  • 左值引用不能直接引用右值,但是const左值引用可以引用右值
  • 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
  • move是库里面的⼀个函数模板,本质内部是进行强制类型转换(将左值强转成右值引用类型)
  • 需要注意的是变量表达式都是左值属性,也就意味着 一个右值被右值引用绑定后,右值引用变量 的变量表达式的属性是左值
  • 语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看下面代码中r1和rr1汇编层实现,底层都是用指针实现的,没什么区别。 底层汇编等实现和上层语法表达的意义有时是背离的,所以不要混到⼀起去理解,互相佐证,这样反而是陷入迷途

(1) 示例一: 左值引用就是给左值取别名,右值引用就是给右值取别名。左值引用不能直接引用右值,但是const左值引用可以引用右值;右值引用不能直接引用左值,但是右值引用可以引用 move(左值)

cpp 复制代码
#include <string>
using namespace std;

int add(int x, int y)
{
    int z = x + y;
    return z;
}

int main()
{
    // 左值:可以取地址
    // 以下的p、b、c、*p、s、s[0]就是常见的左值
    int *p = new int(0);
    int b = 1;
    const int c = b;
    *p = 10;
    string s("111111");
    s[0] = 'x';

    // 1.左值引用给左值取别名
    int &r1 = b;
    int *&r2 = p;
    int &r3 = *p;
    string &r4 = s;
    char &r5 = s[0];

    // 2.右值引⽤给右值取别名
    int &&rr1 = 10;
    double &&rr2 = x + y;
    double &&rr3 = add(x, y);
    string &&rr4 = string("11111");

    // 3.左值引用不能直接引用右值,但是const左值引用可以引用右值
    const int &rx1 = 10;
    const double &rx2 = x + y;
    const double &rx3 = add(x, y);
    const string &rx4 = string("11111");

    // 4.右值引用不能直接引用左值,但是右值引用可以引用 move(左值)
    int &&rrx1 = move(b);
    int *&&rrx2 = move(p);
    int &&rrx3 = move(*p);
    int &&rrx4 = (int &&)*p;
    string &&rrx5 = move(s);
    string &&rrx6 = (string &&)s;

    return 0;
}

// move(左值):其实就相当于将 左值 强制转换成 右值引用类型

cpp 复制代码
string s("111111"); // s是左值

// 右值引用不能直接引用左值,但是右值引用可以引用 move(左值)
// move(左值):其实就相当于将 左值 强制转换成 右值引用类型
// 以下两种写法效果等同
string &&rrx5 = move(s);
string &&rrx6 = (string &&)s;

(2) 示例二: 变量表达式都是左值属性,也就意味着 一个右值被右值引用绑定后,右值引用变量 的变量表达式的属性是左值

cpp 复制代码
using namespace std;

int main()
{
    int b = 1;

    // 左值引用给左值取别名
    int &r1 = b;
    int &pr1 = r1;

    // 右值引用给右值取别名
    int &&rr1 = 10;

    // 这里要注意的是,rr1的属性是左值,所以不能再被右值引用绑定,除非move⼀下(或强制类型转换成 右值引用类型)
    // 错误写法:int &&prr1 = rr1;(会报错)
    int &&prr1 = move(rr1);
    int &&prr2 = (int &&)rr1;

    return 0;
}

// 编译器故意为之的设计,让右值引用变量 具有左值属性,就能直接修改引用的内容,正是这个特性创造了 移动语义!

弊端就是 右值引用变量 具有左值属性,不能再被右值引用绑定,除非move一下(或强制类型转换成 右值引用类型)

  • 这里最让人疑惑的点是:rr1不是本来就是右值引用类型嘛?rr1直接被右值引用绑定就会报错
    将rr1强制类型转换成 右值引用类型,再被右值引用绑定就可以
    这也太神奇了,将右值引用类型 强制类型转换成 右值引用类型,强制类型转换前后有区别嘛???

也不需要太纠结,记住以下结论即可:强制类型转换成右值引用类型 的变量 具有右值属性;而普通的右值引用变量 具有左值属性

cpp 复制代码
int &&rr1 = 10;

// 错误写法:int &&prr1 = rr1;(会报错)
int &&prr1 = move(rr1);
int &&prr2 = (int &&)rr1;

3. 引用延长生命周期

右值引用可用于为 临时对象/匿名对象 延长生命周期;const 的左值引用也能延长 临时对象/匿名对象 生命存期,但这些对象无法被修改。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main()
{
    string s1 = "Test";
    
    // operator+() 的返回值是 string类型的临时对象
    const string &r2 = s1 + s1; // const的左值引用延长 临时对象的生命存期
    // r2 += "Test"; // 但不能通过到 const 的引用修改

    std::string &&r3 = s1 + s1; // 右值引用延长 临时对象的生命存期
    cout << r3 << endl;
    r3 += "Test";               // 能通过到⾮ const 的引用修改
    cout << r3 << endl;
    return 0;
}

4. 左值和右值的参数匹配

  • C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
  • C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配 f(左值引用),实参是const左值会匹配 f(const 左值引用),实参是右值会匹配 f(右值引用)。
  • 右值引用变量在用于表达式时属性是左值(强制类型转换成右值引用类型 的变量 具有右值属性;而普通的右值引用变量 具有左值属性)

(1) 示例一: 当只重载了 左值引用 和 const左值引用作为形参的 f函数时,实参是左值会匹配 f(左值引用),实参是右值 或 const 左值 会匹配 f(const 左值引用)

cpp 复制代码
#include <iostream>
using namespace std;

void f(int &x)
{
    std::cout << "左值引用重载 f(int&)" << endl;
}

void f(const int &x)
{
    std::cout << "到 const 的左值引用重载 f(const int&)" << endl;
}

int main()
{
    int i = 1;
    const int ci = 2;
    f(i);            // 调用 f(int&)
    f(ci);           // 调用 f(const int&)
    f(3);            // 调用 f(const int&)
    f(move(i));      // 调用 f(const int&)

    // 右值引用变量在用于表达式时是左值
    int &&x = 1;
    f(x);            // 调用 f(int& x)
    f(move(x));      // 调用 f(const int&)
    return 0;
}

(2) 示例二: 当分别重载了 左值引用、const左值引用、右值引用作为形参的f函数时,那么实参是左值会匹配 f(左值引用),实参是const左值会匹配 f(const 左值引用),实参是右值会匹配 f(右值引用)

cpp 复制代码
#include <iostream>
using namespace std;

void f(int &x)
{
    std::cout << "左值引用重载 f(int&)" << endl;
}

void f(const int &x)
{
    std::cout << "到 const 的左值引用重载 f(const int&)" << endl;
}

void f(int &&x)
{
    std::cout << "右值引用重载 f(int&&)" << endl;
}

int main()
{
    int i = 1;
    const int ci = 2;
    f(i);            // 调用 f(int&)
    f(ci);           // 调用 f(const int&)
    f(3);            // 调用 f(int&&); 如果没有 f(int&&) 重载, 则会调用 f(const int&)
    f(move(i));      // 调用 f(int&&)

    // 右值引用变量在用于表达式时是左值
    int &&x = 1;
    f(x);            // 调用 f(int& x)
    f(move(x));      // 调用 f(int&& x)
    return 0;
}

三、移动语义

1. 左值引用的使用场景 及其 局限性(有些场景必须 传值返回)

左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。(使用场景链接

左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如addStrings和generate函数,C++98中的解决方案只能是被迫使用输出型参数解决。

那么C++11以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法挽救对象已经析构销毁的事实。

函数中创建的局部对象 无法 传引用返回,因为函数结束这个对象就析构销毁了,引用的对象都被销毁了,传引用返回就毫无意义了(函数中创建的局部对象 只能 传值返回)

cpp 复制代码
#include <algorithm>
#include <vector>
#include <string>
using namespace std;

string addStrings(string num1, string num2)
{
    string str;

    int end1 = num1.size() - 1, end2 = num2.size() - 1;
    int next = 0;
    while (end1 >= 0 || end2 >= 0)
    {
        int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
        int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
        int ret = val1 + val2 + next;
        next = ret / 10;
        ret = ret % 10;
        str += ('0' + ret);
    }
    if (next == 1)
        str += '1';
    reverse(str.begin(), str.end());

    return str;
}

// 这里的传值返回拷贝代价就太⼤了
vector<vector<int>> generate(int numRows)
{
    vector<vector<int>> vv(numRows);

    for (int i = 0; i < numRows; ++i)
    {
        vv[i].resize(i + 1, 1);
    }
    for (int i = 2; i < numRows; ++i)
    {
        for (int j = 1; j < i; ++j)
        {
            vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
        }
    }

    return vv;
}

2. 移动构造 和 移动赋值(移动构造 与 拷贝构造 的对比)

  • 移动构造函数是⼀种构造函数,类似拷贝构造函数,移动构造函数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
  • 移动赋值是⼀个赋值运算符的重载,他跟拷贝赋值构成函数重载,移动赋值函数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
  • 对于像 string/vector 这样的深拷贝的类 或者 包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引用的类型,他的本质是要 "窃取" 引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。 下面的 my_string类(模拟实现string类) 样例实现了移动构造和移动赋值,我们需要结合场景理解。

(1) 模拟实现 STL中的string类(只实现了部分接口):

// my_string.hpp

cpp 复制代码
#include <iostream>
#include <string.h>
#include <algorithm>
#include <assert.h>
using namespace std;

class my_string
{
public:
    typedef char *iterator;
    typedef const char *const_iterator;
    iterator begin()
    {
        return _str;
    }

    iterator end()
    {
        return _str + _size;
    }

    const_iterator begin() const
    {
        return _str;
    }

    const_iterator end() const
    {
        return _str + _size;
    }

    // 构造函数
    my_string(const char *str = "")
        : _size(strlen(str)), _capacity(_size)
    {
        cout << "string(char* str)-构造" << endl;
        _str = new char[_capacity + 1];
        strcpy(_str, str);
    }

    void swap(my_string &s)
    {
        std::swap(_str, s._str);
        std::swap(_size, s._size);
        std::swap(_capacity, s._capacity);
    }

    // 拷贝构造
    my_string(const my_string &s)
    {
        cout << "string(const string& s) -- 拷贝构造" << endl;
        reserve(s._capacity);
        for (auto ch : s)
        {
            push_back(ch);
        }
    }

    // 移动构造
    my_string(my_string &&s)
    {
        cout << "string(string&& s) -- 移动构造" << endl;
        swap(s);
    }

    // 拷贝赋值
    my_string &operator=(const my_string &s)
    {
        cout << "string& operator=(const string& s) -- 拷⻉赋值" << endl;
        if (this != &s)
        {
            _str[0] = '\0';
            _size = 0;
            reserve(s._capacity);
            for (auto ch : s)
            {
                push_back(ch);
            }
        }
        return *this;
    }

    // 移动赋值
    my_string &operator=(my_string &&s)
    {
        cout << "string& operator=(string&& s) -- 移动赋值" << endl;
        swap(s);
        return *this;
    }

    ~my_string()
    {
        cout << "~string() -- 析构" << endl;
        delete[] _str;
        _str = nullptr;
    }

    void reserve(size_t n)
    {
        if (n > _capacity)
        {
            char *tmp = new char[n + 1];
            if (_str)
            {
                strcpy(tmp, _str);
                delete[] _str;
            }
            _str = tmp;
            _capacity = n;
        }
    }

    void push_back(char ch)
    {
        if (_size >= _capacity)
        {
            size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
            reserve(newcapacity);
        }
        _str[_size] = ch;
        ++_size;
        _str[_size] = '\0';
    }
    
    char &operator[](size_t pos)
    {
        assert(pos < _size);
        return _str[pos];
    }

    my_string &operator+=(char ch)
    {
        push_back(ch);
        return *this;
    }

    size_t size() const
    {
        return _size;
    }

private:
    char *_str = nullptr;
    size_t _size = 0;
    size_t _capacity = 0;
};

对于像 string/vector 这样的深拷贝的类 或者 包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引用的类型,他的本质是要 "窃取" 引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率

移动构造的参数是右值引用,只能接收 临时对象等右值;

临时对象等右值的生命周期只有一行,下一行就会被销毁,所以通过移动构造 调用swap函数,直接快速转移临时对象 中的内容更为高效,而且还不用担心改变临时对象中内容会造成影响(临时对象下一行就会被销毁,对后续操作无影响)

结论: 在移动构造产生前,用string临时对象 构造 string对象 只能通过拷贝构造;
有了移动构造后,用string临时对象 构造 string对象 通过移动构造,string临时对象下一行就会销毁,直接转移string临时对象中内容更为高效!!!

3. 右值引用 和 移动语义 解决传值返回问题

cpp 复制代码
#include "my_string.hpp"

my_string addStrings(my_string num1, my_string num2)
{
    my_string str;

    int end1 = num1.size() - 1, end2 = num2.size() - 1;
    int next = 0;
    while (end1 >= 0 || end2 >= 0)
    {
        int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
        int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
        int ret = val1 + val2 + next;
        next = ret / 10;
        ret = ret % 10;
        str += ('0' + ret);
    }
    if (next == 1)
        str += '1';
    reverse(str.begin(), str.end());

    return str;
}

3.1 右值对象构造场景

(1) 右值对象构造,只有拷贝构造,没有移动构造的场景(删除 my_string类中移动构造)

  • 下图,左边为不优化的情况(linux下,关闭g++编译器的构造优化)下,两次拷贝构造;
    右边为编译器优化的场景(vs2019 debug环境下编译器对拷贝的优化)下连续步骤中的拷贝合二为一变为一次拷贝构造。

// linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭g++编译器的构造优化

(2) 右值对象构造,有拷贝构造,也有移动构造的场景

  • 下图,左边为不优化的情况(linux下,关闭g++编译器的构造优化)下,两次移动构造;
    右边为编译器优化的场景(vs2019 debug环境下编译器对拷贝的优化)下连续步骤中的拷贝合二为一变为一次移动构造。

str对象不是左值嘛?为啥会通过 移动构造来 构造临时对象?
左边关闭了编译器的构造优化,但还有一些特殊的构造优化无法被关闭。
比如此处, 便是编译器特殊的构造优化的效果:str 是函数中创建的局部对象,在return str语句结束后,str会被销毁回收;
虽然str是左值,但编译器认为在执行 return str 语句时,str 和 右值 很像,都是即将被销毁的对象,可以直接通过 移动构造 转移 str对象的内容,所以 编译器在编译时,move了此处的 str对象,将str强制转换成右值属性, 这样运行时 执行到这条语句时,move(str) 会通过 移动构造来 构造临时对象。

  • 需要注意的是在vs2019的release 和 vs2022的debug和release版本 ,下面代码优化会非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造 (编译器优化效果:str是对ret对象的引用,addStrings函数中对str引用修改,实际上是直接对ret对象修改)。 要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如下图所示

3.2 右值对象赋值场景

(1) 右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景(删除 my_string类中移动构造 和 移动赋值)

  • 下图,左边展示了vs2019 debug 模式下 和 关闭g++编译器的构造优下 ,编译器的处理(这两种场景的结果一样,这证明 vs2019 debug 模式对 右值对象赋值的优化效果 为零):一次拷贝构造,一次拷贝赋值。
    右边是在vs2019的release和vs2022的debug和release版本下 ,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,addStrings函数中对str引用修改,实际上是直接对临时对象修改。 运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。


(2) 右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景

  • 下图,左边展示了vs2019 debug 模式下 和 关闭g++编译器的构造优下 ,编译器的处理(这两种场景的结果一样,这证明 vs2019 debug 模式对 右值对象赋值的优化效果 为零):一次移动构造,一次移动赋值。
    右边是在vs2019的release和vs2022的debug和release版本下 ,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,addStrings函数中对str引用修改,实际上是直接对临时对象修改。 运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

4. 右值引用 和 移动语义在传参中的提效(STL容器的push和insert系列接口 增加右值引用版本)

  • 查看STL文档,我们发现C++11以后 容器的push和insert系列的接口 增加了右值引用版本
  • 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象
  • 当实参是一个右值,容器内部则调用移动构造,将右值对象的资源转移到容器空间中的对象
  • 以示例中模拟实现的my_list类(模拟实现STL中的list类)为例,支持右值引用参数版本的 push_back 和 insert
cpp 复制代码
// my_list类(链表)中 每个节点的类型
template <class T>
struct ListNode
{
    ListNode<T> *_next;
    ListNode<T> *_prev;
    T _data;

    ListNode(const T &data = T())
        : _next(nullptr), _prev(nullptr), _data(data) // 关键语句
    {
    }

    ListNode(T &&data)
        : _next(nullptr), _prev(nullptr), _data(move(data)) // 关键语句
    {
    }
};

// 简化版list类迭代器的实现
template <class T, class Ref, class Ptr>
struct ListIterator
{
    typedef ListNode<T> Node;
    typedef ListIterator<T, Ref, Ptr> Self;
    Node *_node;

    ListIterator(Node *node)
        : _node(node)
    {
    }

    Self &operator++()
    {
        _node = _node->_next;
        return *this;
    }

    Self &operator--()
    {
        _node = _node->_pre;
        return *this;
    }

    Ref operator*()
    {
        return _node->_data;
    }
    // list迭代器的很多接口实现省略
};

// 实现简化版的list类
template <class T>
class my_list
{
    typedef ListNode<T> Node;

    void empty_init()
    {
        _head = new Node();
        _head->_next = _head;
        _head->_prev = _head;
    }

public:
    my_list()
    {
        empty_init();
    }

    typedef ListIterator<T, T &, T *> iterator;
    typedef ListIterator<T, const T &, const T *> const_iterator;

    iterator begin()
    {
        return iterator(_head->_next);
    }

    iterator end()
    {
        return iterator(_head);
    }

    void push_back(const T &x)
    {
        insert(end(), x); // 关键语句
    }

    void push_back(T &&x)
    {
        insert(end(), move(x)); // 关键语句
    }

    iterator insert(iterator pos, const T &x)
    {
        Node *cur = pos._node;
        Node *newnode = new Node(x); // 关键语句
        Node *prev = cur->_prev;
   
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = cur;
        cur->_prev = newnode;
        return iterator(newnode);
    }

    iterator insert(iterator pos, T &&x)
    {
        Node *cur = pos._node;
        Node *newnode = new Node(move(x)); // 关键语句
        Node *prev = cur->_prev;

        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = cur;
        cur->_prev = newnode;
        return iterator(newnode);
    }

private:
    Node *_head;
};

// list类的模拟实现(详见此链接)

四、左值引用 和 右值引用的补充知识

1. 类型分类(了解即可)

  • C++11以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)将亡值(expiring value,简称xvalue)
  • 纯右值是指那些 字面值常量 或 求值结果相当于字面值或是一个不具名的临时对象。 如:42、true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调用,或者整形 a、b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于C++98中的右值。
  • 将亡值是指 返回右值引用的函数的调用表达式 和 转换为右值引用的转换函数的调用表达,如 move(x)、static_cast<X&&>(x)、(X&&)x
  • 泛左值(generalized value,简称glvalue),泛左值包含将亡值左值
  • 值类别 - cppreference.com 和 Value categories这两个关于值类型的 官方文档,有兴趣可以了解细节。

2. 引用折叠

2.1 引用折叠的介绍 和 规则(typedef中的类型操作 构成引用的引用 的场景)

  • C++中不能直接定义引用的引用 ,如 int& && r = i; ,这样写会直接报错。但通过 函数模板typedef中的类型操作 可以构成引用的引用。
  • 通过模板或 typedef 中的类型操作可以构成引用的引用时,这时C++11给出了一个 引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
cpp 复制代码
typedef int& lref;
typedef int&& rref;

int main()
{
    int n = 0;
    lref &r1 = n;  // int& &(左值引用+左值引用),折叠成:int&(左值引用)
                   // 所以 r1 的类型是 int&

    lref &&r2 = n; // int& &&(左值引用+右值引用),折叠成:int&(左值引用)
                   // 所以 r2 的类型是 int&

    rref &r3 = n;  // int&& &(右值引用+左值引用),折叠成:int&(左值引用)
                   // 所以 r3 的类型是 int&

    rref &&r4 = 1; // int&& &&(右值引用+右值引用),折叠成:int&&(右值引用)
                   // 所以 r4 的类型是 int&&
    return 0;
}

2.2 函数模板 构成引用的引用 的场景(实现 万能引用)

  • 像示例一中 f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用
  • Function(T&& t)函数模板程序中,假设 实参是int右值,模板参数T的推导int;实参是int左值,模板参数T的推导int&。再结合引用折叠规则,就实现了实参是右值,实例化出右值引用版本形参的Function;实参是左值,实例化出左值引用版本形参的Function。

(1) 示例一: 函数模板显示实例化

cpp 复制代码
// 由于引用折叠限定,f1实例化以后总是⼀个左值引用
template <class T>
void f1(T &x)
{
}

// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
template <class T>
void f2(T &&x)
{
}

int main()
{
    int n = 0;
    // 没有折叠->实例化为void f1(int& x)
    f1<int>(n);
    f1<int>(0); // 报错

    // 折叠->实例化为void f1(int& x)
    f1<int &>(n);
    f1<int &>(0); // 报错

    // 折叠->实例化为void f1(int& x)
    f1<int &&>(n);
    f1<int &&>(0); // 报错

    // 折叠->实例化为void f1(const int& x)
    f1<const int &>(n);
    f1<const int &>(0);

    // 折叠->实例化为void f1(const int& x)
    f1<const int &&>(n);
    f1<const int &&>(0);

    // 没有折叠->实例化为void f2(int&& x)
    f2<int>(n); // 报错
    f2<int>(0);

    // 折叠->实例化为void f2(int& x)
    f2<int &>(n);
    f2<int &>(0); // 报错

    // 折叠->实例化为void f2(int&& x)
    f2<int &&>(n); // 报错
    f2<int &&>(0);
    return 0;
}

(2) 示例二: Function(T&& t)函数模板程序中,假设 实参是int右值,模板参数T的推导int;实参是int左值,模板参数T的推导int&。再结合引用折叠规则,就实现了实参是右值,实例化出右值引用版本形参的Function;实参是左值,实例化出左值引用版本形参的Function

// 函数模板中的 T&& t 被称为 万能引用

cpp 复制代码
#include <iostream>
using namespace std;

template <class T>
void Function(T &&t)
{
    int a = 0;
    T x = a;
    // x++;
    cout << &a << endl;
    cout << &x << endl;
}

int main()
{
    // 10是右值,推导出T为int,模板实例化为void Function(int&& t)
    Function(10); // 右值

    //////////////////////////////////////////////////////////////////
    int a;
    // a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
    Function(a); // 左值

    // move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
    Function(move(a)); // 右值
    
    //////////////////////////////////////////////////////////////////
    const int b = 8;
    // b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int&t)
    // 此时Function内部,x++语句会导致报错
    Function(b); // const 左值

    // move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
    // 此时Function内部,x++语句会导致报错
    Function(move(b)); // const 右值
    return 0;
}

3. 完美转发(搭配 万能引用 使用)

  • 万能引用:Function(T&& arg) 函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数。

  • 变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中 arg 的属性是左值,那么我们把 arg 传递给下一层函数wrapper,那么匹配的都是左值引用版本的wrapper函数。这里我们想要恢复 arg对象的属性,就需要使用完美转发实现。

  • 完美转发forward本质是⼀个函数模板,他主要还是通过引用折叠的方式实现,下面示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部arg 被强制类型转换为右值引用返回;传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部arg被强转为左值引用返回。

(1) 如果没有完美转发,左值引用类型arg 和 右值引用类型arg 都是左值属性,全会执行左值引用版本的wrapper函数

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
void wrapper(T &&arg)
{
    // ...
}

template <>
void wrapper<int &>(int &arg)
{
    cout << "左值引用版本" << endl;
}

template <>
void wrapper<int>(int &&arg)
{
    cout << "右值引用版本" << endl;
}

template <typename T>
void function(T &&arg)
{
    wrapper(arg); // int &arg:左值引用类型的arg 的属性是左值,走左值引用版本的wrapper
}                  // int &&arg:右值引用类型的arg 的属性是左值,走左值引用版本的wrapper

int main()
{
    int x = 10;
    function(x);  // 传入左值 → T 推导为 int&
    function(20); // 传入右值 → T 推导为 int
    return 0;
}

(2) 完美转发forward的效果:使 右值引用类型arg 恢复右值属性!!!

有了完美转发,左值引用类型arg 会执行左值引用版本的wrapper函数;右值引用类型arg 会执行右值引用版本的wrapper函数

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
void wrapper(T &&arg)
{
    // ...
}

template <>
void wrapper<int &>(int &arg)
{
    cout << "左值引用版本" << endl;
}

template <>
void wrapper<int>(int &&arg)
{
    cout << "右值引用版本" << endl;
}

template <typename T>
void function(T &&arg)
{
    wrapper(forward<T>(arg)); // int &arg:左值引用类型的arg 的属性是左值,
}                              // forward<int &>(arg) 对 arg不做额外处理。
                               // int &&arg:右值引用类型的arg 的属性是左值,
int main()                     // forward<int>(arg) 对 arg进行 move处理,恢复arg的右值属性。
{
    int x = 10;
    function(x);  // 传入左值 → T 推导为 int&
    function(20); // 传入右值 → T 推导为 int
    return 0;
}
  • forward< T >(arg) 的条件转换:

功能:根据模板参数 T 的类型,决定是否将参数 强制类型转换 为右值引用。

若 T 是左值引用(如 int&),返回左值引用。

若 T 是非引用或右值引用(如 int 或 int&&),返回 强制类型转换的右值引用(等效于 move操作)。

五、可变参数模板

1. 基本语法及原理

  • C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
cpp 复制代码
(1)template <class ...Args> void Func(Args... args) { } // 值传递版本的参数包

(1)template <class ...Args> void Func(Args&... args) { } // 左值引用版本的参数包

(1)template <class ...Args> void Func(Args&&... args) { } // 万能引用版本的参数包
  • 我们用省略号来指出⼀个模板参数或函数参数的表示一个包,在模板参数列表中,class... 或 typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
  • 可变参数模板的原理跟模板类似,本质 还是去实例化对应类型和个数的多个函数。

(1) 使用 sizeof...运算符去计算参数包中参数的个数

cpp 复制代码
#include <iostream>
using namespace std;

template <class... Args>
void Print(Args &&...args)
{
    cout << sizeof...(args) << endl;
}

int main()
{
    double x = 2.2;
    Print();                        // 包里有0个参数
    Print(1);                       // 包里有1个参数
    Print(1, string("xxxxx"));      // 包里有2个参数
    Print(1.1, string("xxxxx"), x); // 包里有3个参数
    return 0;
}

可变参数模板的原理跟模板类似,本质 还是去实例化对应类型和个数的多个函数。如下:

bash 复制代码
// 原理:编译器在底层会结合引用折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);

// 更本质去看,如果没有可变参数模板,我们得实现出多个函数模板才能支持
// 这里的功能,有了可变参数模板,用户进⼀步被解放,可变参数模板是在类型泛化基础
// 上叠加参数数量的变化,让我们泛型编程更加灵活。
void Print();

template <class T1>
void Print(T1&& arg1);

template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);

template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);

2. 包扩展

  • 对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供用于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。底层的实现细节如 示例一的图解所示。
  • C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。

(1) 示例一: 第一种包扩展方式

cpp 复制代码
#include <iostream>
using namespace std;

void ShowList()
{
    // 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
    cout << endl;
}

template <class T, class... Args>
void ShowList(T x, Args... args)
{
    cout << x << " ";
    // args是N个参数的参数包
    // 调用ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包
    ShowList(args...);
}

// 编译时递归推导解析参数
template <class... Args>
void Print(Args... args)
{
    ShowList(args...);
}

int main()
{
    Print(1, string("xxxxx"), 2.2);
    return 0;
}


(2) 示例二: 第二种包扩展方式

cpp 复制代码
#include <iostream>
using namespace std;

template <class T>
const T &GetArg(const T &x)
{
    cout << x << " ";
    return x;
}

template <class... Args>
void Arguments(Args... args)
{
}

template <class... Args>
void Print(Args... args)
{
    // 注意GetArg函数必须返回 获得到的对象,这样才能组成参数包给Arguments
    Arguments(GetArg(args)...);
}
// 本质可以理解为编译器编译时,包的扩展模式
// 将上面的函数模板扩展实例化为下面的函数:
// void Print(int x, string y, double z)
// {
//     Arguments(GetArg(x), GetArg(y), GetArg(z));
// }

int main()
{
    Print(1, string("xxxxx"), 2.2);
    return 0;
}

3. empalce系列接口(empalce系列接口 比 insert和push系列接口高效)

  • C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数 功能上兼容push和insert系列,但是empalce还支持新玩法, 假设容器为container,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。


cpp 复制代码
template <class... Args> 
void emplace_back (Args&&... args);

template <class... Args> 
iterator emplace (const_iterator position, Args&&... args);
  • emplace_back总体而言是更高效,推荐以后使用 emplace系列 替代 insert 和 push系列
  • 传递参数包过程中,如果是 Args&&... args (万能引用)的参数包,要用完美转发参数包,方式如下: std::forward(args)... ,否则编译时包扩展后右值引用变量表达式就变成了左值。
  • 我们模拟实现了list的emplace和emplace_back接口,这里把参数包不断往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前面说的empalce支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。

// my_list.h (补充 emplace_back接口 的实现)

cpp 复制代码
// my_list类(链表)中 每个节点的类型
template <class T>
struct ListNode
{
    ListNode<T> *_next;
    ListNode<T> *_prev;
    T _data;

    ListNode(const T &data = T())
        : _next(nullptr), _prev(nullptr), _data(data)
    {
    }

    ListNode(T &&data)
        : _next(nullptr), _prev(nullptr), _data(move(data))
    {
    }
    
    template <class... Args>  // emplace_back接口把参数包不断往下传递,
    ListNode(Args&&... args)  // 最终在结点的构造中直接去匹配容器存储的数据类型T的构造
    : _next(nullptr), _prev(nullptr), _data(std::forward<Args>(args)...) 
    {
    }
};

// 简化版list类迭代器的实现
template <class T, class Ref, class Ptr>
struct ListIterator
{
    typedef ListNode<T> Node;
    typedef ListIterator<T, Ref, Ptr> Self;
    Node *_node;

    ListIterator(Node *node)
        : _node(node)
    {
    }

    Self &operator++()
    {
        _node = _node->_next;
        return *this;
    }

    Self &operator--()
    {
        _node = _node->_pre;
        return *this;
    }

    Ref operator*()
    {
        return _node->_data;
    }
    // list迭代器的很多接口实现省略
};

// 实现简化版的list类
template <class T>
class my_list
{
    typedef ListNode<T> Node;

    void empty_init()
    {
        _head = new Node();
        _head->_next = _head;
        _head->_prev = _head;
    }

public:
    my_list()
    {
        empty_init();
    }

    typedef ListIterator<T, T &, T *> iterator;
    typedef ListIterator<T, const T &, const T *> const_iterator;

    iterator begin()
    {
        return iterator(_head->_next);
    }

    iterator end()
    {
        return iterator(_head);
    }

    void push_back(const T &x) // push_back接口
    {
        insert(end(), x);
    }

    void push_back(T &&x)
    {
        insert(end(), move(x));
    }

    iterator insert(iterator pos, const T &x)
    {
        Node *cur = pos._node;
        Node *newnode = new Node(x);
        Node *prev = cur->_prev;

        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = cur;
        cur->_prev = newnode;
        return iterator(newnode);
    }

    iterator insert(iterator pos, T &&x)
    {
        Node *cur = pos._node;
        Node *newnode = new Node(move(x));
        Node *prev = cur->_prev;

        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = cur;
        cur->_prev = newnode;
        return iterator(newnode);
    }

    template <class... Args> // 新增 emplace_back 接口
    void emplace_back(Args &&...args)
    {
        insert(end(), std::forward<Args>(args)...);  // emplace_back接口把参数包不断往下传递
    }

    template <class... Args>
    iterator insert(iterator pos, Args &&...args)
    {
        Node *cur = pos._node;
        Node *newnode = new Node(std::forward<Args>(args)...); 
        Node *prev = cur->_prev;

        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = cur;
        cur->_prev = newnode;
        return iterator(newnode);
    }

private:
    Node *_head;
};

(1) 示例一: 前两种情况,push_back 和 emplace_back效率一样;第三种情况,emplace_back效率更高,所以emplace_back总体而言是更高效的

cpp 复制代码
#include "my_list.h"
#include <string>
using namespace std;

int main()
{
    my_list<string> lt;
    string s1("111111111111");
    // 传左值,跟push_back⼀样,⾛拷贝构造
    lt.emplace_back(s1);
    
    // 传右值,跟push_back⼀样,⾛移动构造
    lt.emplace_back(move(s1));

    // 直接把构造string参数包往下传,直接⽤string参数包构造string
    // 这里达到的效果是push_back做不到的
    lt.emplace_back("111111111111");
    return 0;
}

(2) 示例二: 前两种情况,push_back 和 emplace_back效率一样;第三种情况,emplace_back效率更高,所以emplace_back总体而言是更高效的

cpp 复制代码
#include "my_list.h"
#include <string>
using namespace std;

int main()
{
    my_list<pair<string, int>> lt1;
    // 跟push_back⼀样
    // 构造pair + 拷⻉/移动构造pair到list的节点中data上
    pair<string, int> kv("苹果", 1);
    lt1.emplace_back(kv);

    // 跟push_back⼀样
    lt1.emplace_back(move(kv));

    // 直接把构造pair参数包往下传,直接⽤pair参数包构造pair
    // 这⾥达到的效果是push_back做不到的
    lt1.emplace_back("苹果", 1);
    return 0;
}

六、新的类功能

1. 默认的移动构造和移动赋值(先看 三.2)

  • 原来C++类中,有6个默认成员函数:构造函数 / 析构函数 / 拷贝构造函数 / 拷贝赋值重载 以及 取地址重载 / const 取地址重载,最重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
  • 如果你没有自己实现移动构造函数,且没有显式实现析构函数 、拷贝构造、拷贝赋值重载中的任何⼀个。那么编译器会自动生成⼀个默认的移动构造。
    默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝;对于自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有显式实现析构函数 、拷贝构造、拷贝赋值重载中的任何⼀个,那么编译器会自动生成一个默认的移动赋值。
    默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝;对于自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
  • 如果显式实现了移动构造 或 移动赋值中的任意一个,当用户未显示实现拷贝构造 和 拷贝赋值时,编译器不会自动生成默认的拷贝构造 和 拷贝赋值。

2. defult和delete

  • C++11可以让你更好的控制要使用的默认函数。 假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。 比如:我们显示实现了拷贝构造,编译器就不会生成默认的移动构造了,那么我们 可以使用 default关键字 显式指定 编译器生成默认的移动构造
  • 如果能想要限制某些默认函数的生成 ,在C++98中,是该函数设置成private,这样只要其他人想要调用就会报错。
    在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

(1) 示例一: 我们显示实现了拷贝构造,编译器就不会生成默认的移动构造了,那么我们可以使用 default关键字 显式指定 编译器生成默认的移动构造

cpp 复制代码
#include <string>
using namespace std;

class Person
{
public:
    Person(const char *name = "", int age = 0)
        : _name(name), _age(age)
    {
    }

    Person(const Person &p)
        : _name(p._name), _age(p._age)
    {
    }

    Person(Person &&p) = default; // 使用 default关键字 显式指定 编译器生成默认的移动构造

private:
    string _name;
    int _age;
};

(2) 示例二: 如果能想要限制某些默认函数的生成,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数

cpp 复制代码
#include <string>
using namespace std;

class Person
{
public:
    Person(const char *name = "", int age = 0)
        : _name(name), _age(age)
    {
    }

    Person(const Person& p) = delete;

private:
    string _name;
    int _age;
};

3. final与override

final 关键字在 继承章节 详细介绍过,用于实现一个不能被继承的类。链接如下: 【C++】继承详解

override 关键字在 多态章节 详细介绍过,用于检查虚函数是否构成重写。链接如下: 【C++】多态详解

七、lambda

1. lambda表达式语法

  • lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
    lambda 表达式语法使用层而言没有类型,所以我们一般是用 auto 或者 模板参数定义的对象 去接收 lambda对象。

lambda表达式的格式: [capture-list] (parameters)-> return type { function boby }

  • [capture-list]: 捕捉列表,该列表总是出现在 lambda 函数的开始位置 ,编译器根据[ ]来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉。捕捉列表为空也不能省略。
  • (parameters)参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略
  • ->return type返回值类型 ,用追踪返回类型形式声明函数的返回值类型。没有返回值时,此部分可省略;一般返回值类型明确情况下,也可省略,由编译器对返回值类型进行自动推导。
  • {function boby}函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
cpp 复制代码
#include <iostream>
using namespace std;

int main()
{
    // ⼀个简单的lambda表达式
    // lambda表达式 就是一个对象
    auto add1 = [](int x, int y)->int { return x + y; };
    cout << add1(1, 2) << endl;

    // 1、捕捉列表为空也不能省略
    // 2、参数为空可以省略
    // 3、返回值可以省略,可以通过返回值类型自动推导
    // 4、函数体不能省略
    auto func1 = []
    {
        cout << "hello bit" << endl;
        return 0;
    };
    func1();

    int a = 0, b = 1;
    cout << a << ":" << b << endl;
    auto swap1 = [](int &x, int &y)
    {
        int tmp = x;
        x = y;
        y = tmp;
    };
    swap1(a, b);
    cout << a << ":" << b << endl;
    return 0;
}

2. 捕捉列表

  • lambda 表达式中 默认只能使用 lambda 函数体 和 参数中的变量(参数列表接收的参数变量),如果想使用外层作用域中的变量就需要进行捕捉
  • 第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。如 [x,y,&z] 表示 x和y值捕捉,z引用捕捉。
  • 第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表示隐式值捕捉,在捕捉列表写⼀个&表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。
  • 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。 [=, &x]表示其他变量隐式值捕捉,x引用捕捉;[&, x, y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是 = 或 &,并且 & 混合捕捉时,后面的捕捉变量必须是值捕捉;同理 = 混合捕捉时,后面的捕捉变量必须是引用捕捉。
  • lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用。 这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
  • 默认情况下, lambda 捕捉列表的传值捕捉是被const修饰的(传值捕捉本质是一种拷贝,并且被const修饰了),也就是说传值捕捉过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。 使用该修饰符后,参数列表不可省略(即使参数为空)。

(1) 第一种捕捉方式: 在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割

cpp 复制代码
#include <iostream>
using namespace std;

int main()
{
    int a = 0, b = 0, c = 0, d = 0;
    auto func1 = [a, b, &c,&d]
    {
        // 值捕捉的变量默认被const修饰,不能修改; 引用捕捉的变量可以修改
        // a++;
        // b++;
        c++;
        d++;
    };
    func1(); // 执行lambda对象
    cout << a << '|' << b << '|' << c << '|' << d << endl;
    
    return 0;
}


(2) lambda 捕捉列表的传值捕捉是被const修饰的,也就是说传值捕捉过来的对象不能修改。mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。

cpp 复制代码
#include <iostream>
using namespace std;

int main()
{
    int a = 0, b = 0, c = 0, d = 0;
    auto func1 = [a, b, &c, &d]() mutable
    {
        a++;
        b++;
        c++;
        d++;
    };
    func1(); // 执行lambda对象
    cout << a << '|' << b << '|' << c << '|' << d << endl;

    return 0;
}

(3) 第二种捕捉方式: 在捕捉列表中隐式捕捉(我们在捕捉列表写⼀个=表示隐式值捕捉;在捕捉列表写⼀个&表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量)

cpp 复制代码
#include <iostream>
using namespace std;

int main()
{
    int a = 0, b = 0, c = 0, d = 0;
    // 隐式值捕捉
    auto func1 = [=]() mutable
    {
        a++;
        b++;
        c++;
        d++;
    };
    func1(); // 执行lambda对象
    cout << a << '|' << b << '|' << c << '|' << d << endl;

    // 隐式引用捕捉
    auto func2 = [&]
    {
        a++;
        b++;
        c++;
        d++;
    };
    func2(); // 执行lambda对象
    cout << a << '|' << b << '|' << c << '|' << d << endl;
    return 0;
}


(4) 第三种捕捉方式: 在捕捉列表中混合使用隐式捕捉和显示捕捉(当使用混合捕捉时,第一个元素必须是 = 或 &,并且 & 混合捕捉时,后面的捕捉变量必须是值捕捉;同理 = 混合捕捉时,后面的捕捉变量必须是引用捕捉)

cpp 复制代码
#include <iostream>
using namespace std;

int main()
{
    int a = 0, b = 0, c = 0, d = 0;
    // 隐式值捕捉 + 显示引用捕捉
    auto func1 = [=, &c, &d]() mutable
    {
        a++;
        b++;
        c++;
        d++;
    };
    func1(); // 执行lambda对象
    cout << a << '|' << b << '|' << c << '|' << d << endl;

    // 隐式引用捕捉 + 显示值捕捉
    auto func2 = [&, c, d]() mutable
    {
        a++;
        b++;
        c++;
        d++;
    };
    func2(); // 执行lambda对象
    cout << a << '|' << b << '|' << c << '|' << d << endl;
    return 0;
}

(5) lambda 表达式不能捕捉静态局部变量和全局变量,因为静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用

cpp 复制代码
#include <iostream>
using namespace std;

int m = 0; // 全局变量

int main()
{
    static int n = 0; // 静态局部变量

    auto func1 = []
    {
        m++;
        n++;
    };
    func1(); // 执行lambda对象
    cout << m << '|' << n << endl;
    return 0;
}

静态局部变量 和 全局变量 在程序启动时初始化,内存地址固定,生命周期持续至程序结束。
Lambda 表达式无需通过捕获列表显式捕获它们,编译器在编译时能直接解析其地址。
Lambda 表达式内:对静态/全局变量的修改是直接操作原始内存地址中内容,因此外部作用域的值会同步变化。

变量类型 捕获方式 内部修改是否影响外部 能否被捕获
非静态局部变量 值捕获 [x] 不影响(副本)
非静态局部变量 引用捕获 [&x] 影响
静态局部变量 不捕获 影响 禁止
全局变量 不捕获 影响 禁止

3. lambda的原理

  • lambda 的原理和 范围for 很像,编译后从汇编指令层的角度看,压根就没有 lambda 和 范围for 这样的东西。范围for底层是迭代器,而lambda底层是仿函数对象,也就说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。
  • 仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
cpp 复制代码
#include <iostream>
using namespace std;

class Rate
{
public:
    Rate(int a, int b, int &c, int &d)
        : _a(a),
          _b(b),
          _pc(c),
          _pd(d)
    {
    }

    int operator()(int ret) const
    {
        _pc++;
        _pd++;
        ret = _a + _b + _pc + _pd;
        return ret;
    }

private:
    int _a;
    int _b;
    int &_pc;
    int &_pd;
};

int main()
{
    int a = 0, b = 0, c = 0, d = 0;
    int ret = 0;

    // lambda
    auto function_1 = [a, b, &c, &d](int ret)
    {
        c++;
        d++;
        ret = a + b + c + d;
        return ret;
    };
    cout << function_1(ret) << endl;
    cout << a << '|' << b << '|' << c << '|' << d << endl;

    // 匿名对象
    auto function_2 = Rate(a,b,c,d);
    cout << function_2(ret) << endl;
    cout << a << '|' << b << '|' << c << '|' << d << endl;

    return 0;
}

Lambda表达式在编译期会被转换为一个 类(操作系统自动为每个Lambda表达式生成唯一的类名(也就是说每个Lambda表达式的类类型不同)),该类包含以下核心组件:

(1)operator()重载:

Lambda的函数体被编译为该类的operator()成员函数;

lambda参数列表/返回类型/函数体 就是 仿函数operator()的参数/返回类型/函数体。

若无mutable,operator()为const成员函数(禁止修改值捕获的变量);

若添加mutable,operator()变为非const(允许修改值捕获的副本)。

(2)成员变量存储捕获内容:

捕获列表中的变量会生成对应的类成员变量,捕获方式决定成员类型:

值捕获 → 生成普通成员变量(受operator()的const性约束)。

引用捕获 → 生成引用类型成员变量(T&)。

将const修饰的成员函数称之为const成员函数。 const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对 类的任何成员变量本身的内容进行修改(但如果该成员变量是指针 或 引用类型,该成员变量仍可以对其指向 或 引用的内容进行修改,不受任何影响)
const 修饰Rate类的operator()成员函数,operator()隐含的this指针由 Date * const this 变为 const Date * const this

所以在运行时,Lambda表达式实际上就是一个类对象(匿名对象)

4. lambda的使用场景

  • 在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。
  • lambda 在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等, lambda 的应用还是很广泛的,以后我们会不断接触到。
cpp 复制代码
#include <string>
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;

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

// 函数
bool CompEvaluateLess(const Goods &gl, const Goods &gr)
{
    return gl._evaluate < gr._evaluate;
}

bool CompEvaluateGreater(const Goods &gl, const Goods &gr)
{
    return gl._evaluate > gr._evaluate;
}

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());

    sort(v.begin(), v.end(), CompEvaluateLess);
    sort(v.begin(), v.end(), CompEvaluateGreater);


    // 那么这里直接使用lambda表达式就很方便好用了(不需要提前定义含仿函数类 或 函数)
    // 虽然lambda表达式的底层 就是 仿函数类,但它的仿函数类是编译器编译时自动生成的,
    // 不需要用户费心,所以lambda表达式使用起来非常爽(用户使用时,不用管底层实现)
    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;
}

八、包装器

1. function

  • std::function 是一个类模板,也是一个包装器。
    std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调用对象被称为 std::function 的目标。 若 std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。
  • 以下是 function 的原型,他被定义在 < functional > 头文件中。std::function - cppreference.com 是function的官方文件链接。
cpp 复制代码
template <class T>
class function;  // undefined

template <class Ret, class... Args>
class function<Ret(Args...)>;
  • 函数指针、仿函数、 lambda 等可调用对象的类型各不相同, std::function 的优势就是统⼀类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型,下面的第二个代码样例展示了 std::function 作为map的参数,实现字符串和可调用对象的映射表功能。

(1) 示例一: function(包装器)对 函数指针、仿函数、 lambda对象 以及 类的普通成员函数 和 静态成员函数 进行包装

cpp 复制代码
#include <iostream>
#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;
    }
};

// 普通类
class Plus
{
public:
    static int plusi(int a, int b) // 静态成员函数
    {
        return a + b;
    }

    double plusd(double a, double b) // 普通成员函数
    {
        return a + b;
    }
};

int main()
{
    // 包装各种可调用对象
    function<int(int, int)> f1 = f;
    function<int(int, int)> f2 = Functor();
    function<int(int, int)> f3 = [](int a, int b)
                                 { return a + b; };
    cout << f1(1, 1) << endl;
    cout << f2(1, 1) << endl;
    cout << f3(1, 1) << endl;
    
    // 包装静态成员函数
    // 成员函数要指定类域并且前面加 & 才能获取地址
    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;
    return 0;
}

(2) 示例二: 逆波兰表达式求值(function的应用场景)

// 传统解法: 使用STL中的 stack容器 + switch语句

cpp 复制代码
class Solution
{
public:
    int evalRPN(vector<string> &tokens)
    {
        stack<int> st;
        for (auto &str : tokens)
        {
            if (str == "+" || str == "-" || str == "*" || str == "/")
            {
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();
                switch (str[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(str));
            }
        }
        return st.top();
    }
};

// 进阶解法: 使用STL中的 stack容器 + map映射string和function

// 这种方式的最大优势之一是方便扩展,假设还有其他运算,我们增加map中的映射即可

cpp 复制代码
class Solution
{
public:
    int evalRPN(vector<string> &tokens)
    {
        stack<int> st;
        // function作为map的映射可调用对象的类型
        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; }}};
        for (auto &str : tokens)
        {
            if (opFuncMap.count(str)) // 操作符
            {
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();
                int ret = opFuncMap[str](left, right);
                st.push(ret);
            }
            else
            {
                st.push(stoi(str));
            }
        }
        return st.top();
    }
};

2. bind

  • bind 是⼀个函数模板,它也是⼀个可调用对象的包装器,可以把他看做⼀个函数适配器,对接收的fn可调用对象进行处理后返回一个可调用对象。 bind 可以用来调整参数个数和参数顺序。
  • 以下是 bind 的原型,他被定义在 < functional > 头文件中。std::bind - cppreference.com 是bind的官方文件链接。
cpp 复制代码
template <class Fn, class... Args>
         bind (Fn&& fn, Args&&... args);

template <class Ret, class Fn, class... Args>
         bind (Fn&& fn, Args&&... args);
  • 调用bind的一般形式: auto newCallable = bind(callable,arg_list);
    其中newCallable本身是一个可调用对象,callable是bind要包装的可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。
    当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
  • arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数的位置。 数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。 _1/_2/_3... 这些占位符放到placeholders的一个命名空间中。

(1) 示例一: bind 调整可调用对象的 参数顺序

cpp 复制代码
#include <iostream>
#include <functional>
using namespace std;

using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int sub(int a, int b, int c)
{
    return (a - b - c) * 10;
}

int main()
{
    auto sub0 = bind(sub, _1, _2, _3);
    cout << sub0(10, 5, 1) << endl; // (10 - 5 -1)*10
    // bind 本质返回的⼀个仿函数对象
    // _1代表第⼀个实参
    // _2代表第二个实参
    // _3代表第三个实参

    // 调整参数顺序 (不常用)
    auto sub1 = bind(sub, _2, _1, _3); 
    cout << sub1(10, 5, 1) << endl; // (5 - 10 -1)*10

    auto sub2 = bind(sub, _3, _2, _1);
    cout << sub2(10, 5, 1) << endl; // (1 - 5 -10)*10

    return 0;
}

(2) 示例二: bind 绑死可调用对象的一些固定参数,从而减少传参个数

cpp 复制代码
#include <iostream>
#include <functional>
using namespace std;

using placeholders::_1;
using placeholders::_2;

int sub(int a, int b, int c)
{
    return (a - b - c) * 10;
}

class Plus
{
public:
    double plusd(double a, double b)
    {
        return a + b;
    }
};

int main()
{
    // 分别绑死第1、2、3个参数
    auto sub1 = bind(sub, 100, _1, _2);
    cout << sub1(5, 1) << endl; // (100 - 5 - 1) * 10
    
    auto sub2 = bind(sub, _1, 100, _2);
    cout << sub2(5, 1) << endl; // (5 - 100 - 1) * 10
    
    auto sub3 = bind(sub, _1, _2, 100);
    cout << sub3(5, 1) << endl; // (5 - 1 - 100) * 10

    cout << "---------------------------" << endl;
    // 同时绑死2个参数
    auto sub4 = bind(sub, 100, 10, _1);
    cout << sub4(5) << endl; // (100 - 10 - 5) * 10
    
    auto sub5 = bind(sub, _1, 100, 10);
    cout << sub5(5) << endl;  // (5 - 100 - 10) * 10

    cout << "---------------------------" << endl;
    function<double(Plus *, double, double)> f1 = &Plus::plusd;
    Plus pd;
    cout << f1(&pd, 1.1, 1.1) << endl;
    // 将成员函数的第一个参数进行绑死,就不需要每次都传递了
    function<double(double, double)> f2 = bind(&Plus::plusd, &pd, _1, _2);
    cout << f2(1.1, 1.1) << endl;
    return 0;
}

(3) 示例三: bind 的应用场景(在银行定期存款,3年定期的利率为1.5%,5年定期的利率为2%,10年定期的利率为2.5%,30年定期的利率为3.5%,每种定期存款的年数 与 利率 是固定的,只有存进去的金额在变化,所以可以使用bind绑死年数 与 利率的参数)

cpp 复制代码
#include <iostream>
#include <functional>
using namespace std;

using placeholders::_1;

int main()
{
    // 计算复利的lambda
    auto func = [](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)> func1 = bind(func, 0.015, _1, 3);
    function<double(double)> func2 = bind(func, 0.02, _1, 5);
    function<double(double)> func3 = bind(func, 0.025, _1, 10);
    function<double(double)> func4 = bind(func, 0.035, _1, 30);

    cout << func1(1000000.0) << endl;
    cout << func2(1000000.0) << endl;
    cout << func3(1000000.0) << endl;
    cout << func4(1000000.0) << endl;
    return 0;
}

九、智能指针

后续补充


相关推荐
好评1242 小时前
【C++】一篇吃透容器适配器三件套:从stack/queue/priority_queue到deque底层
c++·stl·queue·stack
深海蓝山2 小时前
WebSocket(java版)服务示例
java·websocket·网络协议
Howe~zZ2 小时前
mybatis 报错解决方案ORA-01795: maximum number of expressions in a list is 1000
java·服务器·前端
LiamTuc2 小时前
Java 抽象类详解
java·开发语言
计算机学姐2 小时前
基于Python的高校后勤报修系统【2026最新】
开发语言·vue.js·后端·python·mysql·django·flask
GSDjisidi2 小时前
日本IT行业|一些it資格证书分享解析,一篇通读
开发语言·面试·职场和发展
南山乐只2 小时前
Spring Boot 2.x => 3.x 升级指南
java·spring boot·后端
任子菲阳2 小时前
学Java第五十五天——多线程&JUC
java·开发语言
yaoxin5211232 小时前
265. Java 集合 - LinkedList vs ArrayList 插入性能实战对比分析
java·开发语言