当前位置: 首页 > news >正文

【探寻C++之旅】C++11 深度解析:重塑现代 C++ 的关键特性

QQ20251015-150955

请君浏览

    • 前言
    • 1. C++的发展历史
    • 2. 列表初始化:统一对象初始化的优雅方案
      • 2.1 从 C++98 到 C++11 的突破
      • 2.2 std::initializer_list:容器初始化的 “神器”
    • 3. 右值引用和移动语义:彻底解决拷贝性能痛点
      • 3.1 左值 vs 右值
      • 3.2 左值引用 vs 右值引用
      • 3.3右值引用的使用场景
        • 3.3.1参数匹配
        • 3.3.2 类型分类
        • 3.3.3 移动构造和移动赋值
      • 3.4 引用折叠
        • 3.4.1 万能引用
        • 3.4.2 完美转发
    • 4. 可变参数模板:灵活处理任意参数
      • 4.1 基本语法及原理
      • 4.2 包扩展
      • 4.3 emplace系列接口
    • 5. 新的类功能:让类定义更简洁,行为控制更精准
      • 5.1 默认的移动构造和移动赋值
      • 5.2 defult和delete
      • 5.3 其他
    • 6. lambda表达式:匿名函数的优雅实现
      • 6.1 基本语法
      • 6.2 捕捉列表
      • 6.3 应用场景
      • 6.4 原理
    • 7. 包装器:统一可调用对象类型
      • 7.1 function:统一可调用对象类型
      • 7.2 bind:调整可调用对象的参数
    • 8. 小结
    • 尾声

前言

作为 C++ 开发者,你是否曾为对象初始化的混乱语法头疼?是否因频繁的拷贝操作导致性能瓶颈?如果你的答案是 “是”,那 C++11 绝对是你必须掌握的里程碑版本 —— 它不仅解决了 C++98/03 时代的诸多痛点,更奠定了现代 C++ 的编程范式。今天,就带你系统梳理 C++11 的核心特性,帮你真正理解 “为什么 C++11 是 C++98 之后最重要的更新”。下面让我们一起来进入本章的学习。

1. C++的发展历史

我们知道一门语言不是一成不变的,它会不断的更新,那么对于C++来说,第一次重要的版本更新就是在1998年推出的C++98。C++98 作为 C++ 的第一个正式国际标准(ISO/IEC 14882:1998),奠定了现代 C++ 最核心的基础框架。后续所有版本(C++03 到 C++23)的演进,都是在这个基础上的扩展、优化和补充,

C++ 的发展以ISO 国际标准为核心节点,每个版本都针对性解决前一阶段的痛点,逐步迈向 “现代 C++”:

QQ20251016-002105

C++11 并非一蹴而就,它最初被称为 “C++0x”—— 因为开发者原本期待它能在 2010 年前发布。直到 2011 年 8 月 12 日,ISO 才正式采纳这一标准,命名为 “C++11”。它与前一版本 C++03 间隔了 8 年,是迄今为止 C++ 版本迭代中间隔最长的一次,也正因如此,它凝聚了大量关键改进:标准化了当时已有的实践(如 STL 的进一步优化),同时引入了全新的抽象机制(如移动语义、lambda)。

从 C++11 开始,C++ 进入了 “三年一更新” 的规律迭代周期(C++14、C++17、C++20、C++23 依次发布),而 C++11 正是这一切的起点。下面让我们来看一看C++11中到底有哪些**“新东西”**。

2. 列表初始化:统一对象初始化的优雅方案

2.1 从 C++98 到 C++11 的突破

在 C++98 中,对象初始化的语法堪称 “混乱”:数组用{},结构体用{},但类对象却只能用构造函数 +()=,这种不一致性让开发者频繁查阅文档。而 C++11 的列表初始化(又称{}初始化)解决了这个问题,核心目标是 “一切对象皆可通过{}初始化”。

C++98 仅支持数组和结构体的{}初始化,例如:

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

但如果是自定义类(如Date),就无法用{}初始化 —— 这在 C++11 中被彻底改变。

C++11 的列表初始化的关键在于支持所有对象使用{}进行初始化:不管是内置类型(int x {2};)还是自定义类型(Date d {2024, 10, 1};)均适用;并且使用列表初始化时可以不加=

下面让我们通过代码来感受一下:

#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()
{Date d1 = { 2025, 1, 1};const Date& d2 = { 2024, 7, 25 };return 0;
}

就像上述代码中的d1,他的本质是⽤{ 2025, 1, 1}构造⼀个Date的临时对象,然后临时对象再去拷⻉构造d1,不过有的编译器会对这种“直接构造+拷贝构造”的形式进行优化,优化成直接构造:

QQ20251019-225651

因为使用列表初始化会创建一个临时对象,因此这里d2引用的是{ 2024, 7, 25 }构造的临时对象,因此我们可以看到我们d2虽然是一个引用的别名,但Date还是进行了构造。

需要注意的是C++98⽀持单参数时的类型转换,也就是说当参数只有一个时也可以不⽤{} 进行初始化,例如下面的方式在C++98中是被支持的:Date d3 = 2025;

在使用列表初始化时是可以省略=的:

int main()
{Point p1 { 1, 2 };int x1 { 2 };Date d4 { 2024, 7, 25 };const Date& d5 { 2024, 7, 25 };return 0;
}

只有使用列表初始化时才可以省略等号,例如下面这种方式是不可以的:Date d6 2025;

C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下还可以带来不少便利,例如在容器中push/insert多参数构造的对象时,使用{}初始化会很⽅便:

int main()
{vector<Date> v;//有名对象Date d7 = { 2024, 7, 25 };v.push_back(d7);//匿名对象v.push_back(Date(2025, 1, 1));// ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐v.push_back({ 2025, 1, 1 });return 0;
}

有了列表初始化后,我们对于需要使用多参数构造的对象传参时便不用像之前一样只能使用有名对象和匿名对象传参,而是直接使用列表初始化,可以简化我们的代码。

2.2 std::initializer_list:容器初始化的 “神器”

列表初始化的另一大贡献,是引入了std::initializer_list类模板 —— 它让 STL 容器的初始化变得前所未有的简单。上⾯的初始化已经很⽅便了,但是对于对象容器初始化还是不太⽅便,比如⼀个vector对象,在 C++98 中,要初始化他,你可能需要这样写:

vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);

当我们想用N个值去构造初始化,那么我们得实现很多个构造函数才能⽀持,例如我们想要实现下面两种构造场景:

vector<int> v1 = {1,2,3};
vector<int> v2 = {1,2,3,4,5};

我们就需要写这两种不同参数所对应的构造函数。而C++11借助std::initializer_list,直接支持 “字面量式” 初始化,也就是说我们上述两种方式的初始化都可以支持。他的本质是STL 容器(如vectormap)在 C++11 中新增了接收std::initializer_list的构造函数,std::initializer_list也叫做初始化列表,他是C++11新增的类,这个类的本质是底层开⼀个数组,将数据拷贝,std::initializer_list内部有两个指针分别指向数组的开始和结束。

auto il = { 10, 20, 30 }; 
// the type of il is an initializer_list

这是他的⽂档:[initializer_list](initializer_list - C++ Reference),感兴趣的可以自行查看。 std::initializer_list⽀持迭代器遍历。

容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的{x1,x2,x3...}进行初始化。STL中的容器⽀持任意多个值构成的{x1,x2,x3...}进⾏初始化,就是通过容器中参数为std::initializer_list的构造函数⽀持的。

另外,容器的赋值也⽀持initializer_list的版本:

v = {10, 20, 30};  // 直接用列表更新容器内容

下面让我们通过具体的代码来认识一下initializer_list

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

下面是运行结果:
QQ20251019-233154

有了initializer_list,我们对于容器的初始化就变得简单了:

#include<iostream>
#include<vector>
#include<string>
#include<map>
using namespace std;
int main()
{// {}列表中可以有任意多个值// 这两个写法语义上还是有差别的,第⼀个v1是直接构造// 第⼆个v2是构造临时对象+临时对象拷⻉优化为直接构造vector<int> v1({ 1,2,3,4,5 });vector<int> v2 = { 1,2,3,4,5 };const vector<int>& v3 = { 1,2,3,4,5 };// 这⾥是pair对象的{}初始化和map的initializer_list构造结合到⼀起⽤了map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };return 0;
}

3. 右值引用和移动语义:彻底解决拷贝性能痛点

如果说列表初始化解决了 “语法优雅” 问题,那右值引用与移动语义就是 C++11 解决 “性能瓶颈” 的核心 —— 它针对 “临时对象的冗余拷贝” 这一痛点,提供了根本性的优化方案。C++98的语法中就有引⽤的语法,⽽C++11中新增了右值引⽤语法特性,在C++98时的引用,我们称之为左值引用。⽆论左值引⽤还是右值引⽤,都是给对象取别名

3.1 左值 vs 右值

在了解右值引用和移动语义之前,我们首先要了解左值和右值的概念:

  • 左值:有明确内存地址、可长期存在的对象(如变量、解引用的指针),能出现在赋值符号左边;以下的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';
    
  • 右值:临时对象、字面值(如103.14)或表达式结果(如a + b),无持久内存地址,不能出现在赋值符号左边;以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值

    double x = 1.1, y = 2.2;
    10;
    x + y;
    fmin(x, y);
    string("11111");
    

简单来说,如果对一个可以取它的地址,那么它就是左值,反之则是右值。

值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是left valueright value 的缩写。现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,⽽ rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址

3.2 左值引用 vs 右值引用

顾名思义,对左值进行引用叫做左值引用,那么右值引用就是对右值进行引用。

C++11 新增右值引用(语法:Type&&),专门用于绑定右值(临时对象),并延长其生命周期。与之对应,之前的引用(Type&)被称为 “左值引用”。

Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。

对于左值引用和右值引用需要注意下面几个点:

  • 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值。

    int& r1 = 10;        //error: 10是右值,不能用左值引用去引用右值
    const int& r2 = 10;  //correct
    
  • 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤std::move(左值) 。(std::move()是库中的⼀个函数模板,本质内部是进⾏强制类型转换,不移动任何数据)

    int a = 10;
    int&& rr1 = a;        //error: a是左值,不能被右值引用
    int&& rr2 = move(a);  //correct
    
  • 变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量本身的属性是左值(因为它有内存地址)。

    int&& rr1 = 10;
    int&& rr2 = rr1;        //error: rr1的属性是左值,不能被右值引用
    int&& rr3 = move(rr1);  //correct: 如果想用右值引用引用左值,可以用move将左值强转为右值
    int& r4 = rr1;          //correct
    
  • 引用可以延长变量的生命周期。右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。

    int main()
    {std::string s1 = "Test";// std::string&& r1 = s1;          // error:不能绑定到左值const std::string& r2 = s1 + s1;   // correct:到 const 的左值引⽤延⻓⽣存期// r2 += "Test";                   // error:不能通过到 const 的引⽤修改std::string&& r3 = s1 + s1;        // correct:右值引⽤延⻓⽣存期r3 += "Test";                      // correct:能通过到⾮ const 的引⽤修改std::cout << r3 << '\n';return 0;
    }
    

    QQ20251021-130251

语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看上面代码中r1rr1汇编层实现,底层都是⽤指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要杂糅到⼀起去理解,互相佐证,这样反⽽是陷⼊迷途。

3.3右值引用的使用场景

在了解右值引用的使用场景之前,让我们先来看一看左值和右值的参数匹配以及右值具体的类型分类,以便我们能更好的去理解右值引用的使用场景。

3.3.1参数匹配

在之前我们实现⼀个const左值引⽤作为参数的func函数,那么实参传递左值和右值都可以与之匹配,那么在C++11之后我们分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的func函数,那么实参是左值会匹配func(左值引⽤),实参是const左值会匹配func(const 左值引⽤),实参是右值会匹配func(右值引⽤)。这是因为我们在调用函数时编译器会去寻找最匹配的函数,下面让我们通过代码来看一下:

#include<iostream>
using namespace std;
void f(int& x)
{std::cout << "左值引用重载 f(" << x << ")\n";
} 
void f(const int& x)
{std::cout << "const左值引用重载 f(" << x << ")\n";
} 
void f(int&& x)
{std::cout << "右值引用重载 f(" << x << ")\n";
} 
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(std::move(i));  // 调用 f(int&&)// 右值引用变量在用于表达式时是左值int&& x = 1;f(x);             // 调用 f(int& x)f(std::move(x));  // 调用 f(int&& x)return 0;
}

下面是代码的运行结果:

QQ20251021-132304

3.3.2 类型分类

C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue):

  • 纯右值(prvalue):临时创建的匿名对象(如string("hello"))、字面量(如123)等。
  • 将亡值(xvalue):有身份(可通过地址识别)但即将被销毁的对象(典型例子:函数返回的局部对象、被std::move转换的对象)。

纯右值和将亡值是在C++11中提出的,C++11中的纯右值概念划分等价于C++98中的右值。 此外,在C++11还有泛左值(generalized value,简称glvalue),泛左值的核心特征是可以通过地址识别(即具有 “身份”),不管该表达式是否可修改。泛左值包含将亡值和左值。 可以看到泛左值和右值之间有些部分是重合的,它们之间的关系如下图所示:
QQ20251021-194916

这是关于C++值类型的文档,有兴趣的可以了解细节。

3.3.3 移动构造和移动赋值

左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如下面的 addStringsgenerate 函数:

QQ截图20251021133559

C++98 中的解决方案只能是被迫使用输出型参数解决。那么 C++11 以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法改变对象已经析构销毁的事实。虽然无法使用右值引用返回解决这个问题,但是我们可以使用右值引用来减少传值返回时的开支,这也就是右值引用的最终目的,就是是实现移动语义—— 对于需要深拷贝的类(如stringvector),移动构造 / 赋值会 “窃取” 右值对象的资源(如内存缓冲区),而非重新分配内存并拷贝数据,从而大幅提升性能。

  • **移动构造:**移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。

    string类为例,C++98 中拷贝构造是这样的:

    // 拷贝构造:深拷贝,开销大
    string(const string& s) 
    {_str = new char[s._size + 1];strcpy(_str, s._str);_size = s._size;
    }
    

    而 C++11 的移动构造则 “窃取” 资源:

    // 移动构造:直接交换指针,无拷贝
    string(string&& s) noexcept 
    {swap(_str, s._str);    // 窃取s的内存缓冲区swap(_size, s._size);// s的资源被“掏空”,析构时不会影响当前对象
    }
    
  • **移动赋值:**移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。与移动构造一样,移动赋值也是通过交换资源实现,避免深拷贝,从而减少资源消耗。

对于那些临时对象,虽然我们无法取它们的地址,但它们是占据了内存中的空间,那我们移动语义的目的就是说:当我们要用一个临时对象去创建一个对象,那么我们就不需要再开辟一块空间,而是直接去掠夺临时对象的空间,因为这些临时对象的声明周期只在它自己的当前行,以此做到物尽其用

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

由于右值引用是在C++11才被提出,那么在此之前编译器为了提高拷贝效率,它会自动识别,去优化拷贝的过程,如下图所示:

QQ20251021-201040

可以看到,对于上述代码,函数返回时本应该有两次拷贝构造,但被编译器优化为两次构造。那么在有了移动构造之后也是一样,编译器也会做出相应的优化:

QQ20251021-201654

需要注意的是在vs2019的release和vs2022的debugrelease版本下,上面的代码优化会⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象合三为⼀,变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图所示:

QQ20251021-202445

这时可能有人会说既然编译器可以自己优化,那么移动语义还有什么用呢?虽然说编译器会对这些相应的场景进行优化,但是我们无法确定使用的编译器是否一定会进行优化,但是如果我们使用了移动语义,无论编译器是否优化,效率都会高于没有移动语义的场景。

查看STL⽂档我们可以发现C++11以后容器的pushinsert等系列的接⼝都增加了右值引⽤版本:

QQ20251021-202728

右值引用通过移动语义消除冗余拷贝,同时为 “拷贝” 与 “移动” 提供明确的语义区分,支撑了标准库的性能优化和模板编程的灵活性。它让 C++ 在保持零成本抽象的同时,大幅提升了处理大型对象和临时对象时的效率,是现代 C++ 性能优化的基石。

3.4 引用折叠

C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,但是我们可以通过模板推导、auto类型推导、typedef/using别名定义等构成引⽤的引⽤。当我们通过这些操作构成引⽤的引⽤时,C++11给出了⼀个引⽤折叠的规则:**右值引⽤的右值引用折叠成右值引⽤,所有其他组合均折叠成左值引用。**具体逻辑如下:

  • T& & → 折叠为 T&(左值引用的左值引用 → 左值引用)
  • T& && → 折叠为 T&(左值引用的右值引用 → 左值引用)
  • T&& & → 折叠为 T&(右值引用的左值引用 → 左值引用)
  • T&& && → 折叠为 T&&(右值引用的右值引用 → 右值引用)

下⾯的程序中很好的展示了模板和typedef构成引⽤的引⽤时的引⽤折叠规则:

// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>void f1(T& x)
{}// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>void f2(T&& x)
{}int main()
{typedef int& lref;typedef int&& rref;int n = 0;lref& r1 = n; // r1 的类型是 int&lref&& r2 = n; // r2 的类型是 int&rref& r3 = n; // r3 的类型是 int&rref&& r4 = 1; // r4 的类型是 int&&// 没有折叠->实例化为void f1(int& x)f1<int>(n);f1<int>(0); // error: 指定了类型后推导出是参数是左值引用,左值引用无法引用右值,下面也都同理// 折叠->实例化为void f1(int& x)f1<int&>(n);f1<int&>(0); // error// 折叠->实例化为void f1(int& x)f1<int&&>(n);f1<int&&>(0); // error// 折叠->实例化为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); // errorf2<int>(0);// 折叠->实例化为void f2(int& x)f2<int&>(n);f2<int&>(0); // error// 折叠->实例化为void f2(int&& x)f2<int&&>(n); // errorf2<int&&>(0);return 0;
}

f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左值时就是左值引⽤,传递右值时就是右值引⽤,因此这种函数模板的参数也叫做万能引⽤

3.4.1 万能引用

对于万能引用,我们可以参考下面的代码:

template<class T>
void Function(T&& t)
{int a = 0;T x = a;//x++;cout << &a << endl;cout << &x << endl << endl;
} int main()
{// 10是右值,推导出T为int,模板实例化为void Function(int&& t)Function(10); int a;// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)Function(a); // std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)Function(std::move(a)); const int b = 8;// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int& t)// 所以Function内部会编译报错,x不能++Function(b); // std::move(b)是右值,推导出T为const int,模板实例化为void Function(const int&& t)// 所以Function内部会编译报错,x不能++Function(std::move(b)); return 0;
}

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

3.4.2 完美转发

在上面的Function(T&& t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化以后是右值引⽤的Function函数。但是我们知道变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。如下面代码所示:

void Fun(int& x) { cout << "左值引⽤" << endl; }
void Fun(const int& x) { cout << "const 左值引⽤" << endl; }void Fun(int&& x) { cout << "右值引⽤" << endl; }
void Fun(const int&& x) { cout << "const 右值引⽤" << endl; }template<class T>
void Function(T&& t)
{Fun(t);
} int main()
{// 10是右值,推导出T为int,模板实例化为void Function(int&& t)Function(10); // 右值int a;// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)Function(a); // 左值// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)Function(std::move(a)); // 右值const int b = 8;// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t)Function(b); // const 左值// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)Function(std::move(b)); // const 右值return 0;
}

QQ20251021-205642

这⾥我们想要保持t对象的属性,就需要使⽤完美转发实现。完美转发的本质是⼀个函数模板forward,他主要还是通过引⽤折叠的⽅式实现,上面示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤返回

template<class T>
void Function(T&& t)
{Fun(forward<T>(t));
} 

那么我们在运行之前的代码来看一下结果:

QQ20251021-210127

这样结果就符合我们的预期了。

完美转发(Perfect Forwarding)是 C++ 中用于在函数调用链中保留参数原始值类别(左值 / 右值) 的技术,其核心用途是解决 “参数转发时值类别丢失” 的问题,确保转发后的参数能被目标函数以正确的方式(左值引用接收左值,右值引用接收右值)处理。

4. 可变参数模板:灵活处理任意参数

C++11 支持可变参数模板,允许模板接收 “零或多个参数”(称为 “参数包”),解决了之前模板无法处理不定数参数的问题。

4.1 基本语法及原理

C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:

  • **模板参数包:**表⽰零或多个模板参数
  • **函数参数包:**表⽰零或多个函数参数

语法:template <class... Args>

我们⽤省略号来指出⼀个模板参数或函数参数表示⼀个包:

  • 在模板参数列表中,class...typename...指出接下来的参数表⽰零或多个类型列表;

  • 在函数参数列表中,类型名后⾯跟…指出接下来表⽰零或多个形参对象列表;

  • 函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板⼀样,每个参数实例化时遵循引⽤折叠规则。如下所示:

    template <class ...Args> void Func(Args... args) {}
    template <class ...Args> void Func(Args&... args) {}   //左值引用
    template <class ...Args> void Func(Args&&... args) {}  //万能引用
    

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

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

这⾥我们可以使⽤sizeof...运算符去计算参数包中参数的个数。

下面让我们来看一下运行结果:

QQ20251022-125916

可以看出与我们的预期相符。上面我们说了,可变参数模板的本质与模板相同,都是去实例化对于的函数,对于上面的Print函数,编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数:

void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);

如果没有可变参数模板,我们需要实现出多个函数模板才能⽀持这⾥的功能,而有了可变参数模板,我们进⼀步被解放,他是类型泛化基础上叠加数量变化,让我们泛型编程更灵活。

  • 普通函数模板:

    本来要写多个函数->一个函数模板即可

  • 可变参数函数模板:

    本来要写多个函数模板->一个可变参数函数模板即可

4.2 包扩展

对于⼀个参数包,我们除了能计算他的参数个数,还能扩展它。当扩展⼀个包时,我们需要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(…)来触发扩展操作。如下面代码所示:

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();Print(1);Print(1, string("xxxxx"));Print(1, string("xxxxx"), 2.2);return 0;
}

我们来看运行结果:
QQ20251023-165341

可以看到我们将每一个参数都打印了出来,具体的底层原理如下图所示:

QQ20251023-171206

需要注意的是,我们在扩展参数包时,编译递归的终止条件必须重载相应函数对应参数包为0的参数,而不能直接在函数中判断参数包是否为0来作为递归结束条件,如下列代码是错误的:

template <class t, class ...Args>
void ShowList(t x, Args... args)
{if (sizeof...(args) == 0){return;}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;
}

C++还⽀持更复杂的包扩展:直接将参数包依次展开依次作为实参给⼀个函数去处理。 如下面代码所示:

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必须返回或者到的对象,这样才能组成参数包给ArgumentsArguments(GetArg(args)...);
} int main()
{Print(1, string("xxxxx"), 2.2);return 0;
}

本质可以理解为编译器编译时,包的扩展模式将上⾯的函数模板扩展实例化为下⾯的函数:

void Print(int x, string y, double z)
{Arguments(GetArg(x), GetArg(y), GetArg(z));
}

4.3 emplace系列接口

C++11以后STL容器新增了empalce系列的接⼝,empalce系列接口的参数均为可变模板参数,功能上兼容pushinsert系列。我们以list为例,来看一下emplace系列接口对比pushinsert系列有哪些优势。

  • QQ20251023-172958

    对于push_back来说,它只是一个普通的函数,它的参数在类模板实例化的时候就已经实例化了,value_type就是我们存储在list中值的类型。因此我们可以看到push_back有两个版本,分别为左值和右值版本。

  • QQ20251023-173004

    而对于emplace_back来说,他是一个可变参数函数模板,因此它也是万能引用,而最重要的是它的参数是一个参数包。

emplace_backpush_back而言,当参数的类型与实际的类型相同时是没有区别的,而当参数的类型与实际的参数不同,也就是说需要进行类型转换时,它们的差别就体现出来了,如下图所示:

QQ20251023-210042

也就是说当我们使用emplace系列接口时,参数包不断往下传递 ,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。 而push_back等系列无法做到,这里string是单参数,会进行单参数的隐式类型转换,因此两者在此时的使用方法没有太大差别。那么当list的类型是pair时呢?

lt1.emplace_back("苹果", 1);

对于emplace_back,我们直接将pair的两个参数写入,这样参数包在往下传递时可以直接构造相应的对象。

lt1.push_back({"苹果", 1});

而对于push_back,我们就无法想上面一样,因为push_back的参数只有一个,因此我们需要通过列表初始化先构造一个临时对象才能继续往下进行。

emplace系列总体⽽⾔是更⾼效的,因此在平常的使用中更推荐使用emplace系列去替代insertpush系列。

当我们在自定义类中想要实现emplace系列接口需要注意在传递参数包过程中,如果是 Args&&... args 的参数包,要⽤完美转发参数包,方式如下std::forward<Args>(args)... ,否则编译时包扩展后右值引用变量表达式就变成了左值,导致错误。

可变参数模板的核心是 “包扩展”,通过...将参数包拆解为单个参数。此外,STL 的emplace_back接口也基于可变参数模板实现,能直接在容器中构造对象(无需临时对象),比push_back更高效。

5. 新的类功能:让类定义更简洁,行为控制更精准

5.1 默认的移动构造和移动赋值

在C++98的类中,有6个默认成员函数,分别是:

  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 拷贝赋值重载
  • 取地址重载
  • const 取地址重载

最后重要的是前4个,后两个⽤处不⼤。默认成员函数就是我们不写时编译器会⽣成⼀个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。

  • 如果没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意⼀个。那么编译器会⾃动⽣成⼀个默认的移动构造。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。
  • 如果没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷贝,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷贝赋值。(默认移动赋值跟上⾯移动构造完全类似)

其实也很好理解,当我们的类中没有需要深拷贝的资源时,也就不需要写相应的析构函数和拷贝构造等等,那么也就不需要我们再显示的写移动构造,默认生成的浅拷贝就足够了。而如果需要进行深拷贝,那么我们就需要自己去写对应的移动构造等等,因为默认生成的无法满足需求,所以编译器干脆就不生成默认的。

5.2 defult和delete

在 C++11 中,新增了defaultdelete两个关键字,它们主要用于显式控制类的特殊成员函数(如构造函数、拷贝控制函数等)的生成行为,解决了 C++98 中 “默认函数生成规则不直观”“禁止默认函数需用 hack 手段” 等问题。

**defult:**显式要求编译器生成默认版本的特殊成员函数.

C++ 类会隐式生成一些特殊成员函数(如默认构造函数、拷贝构造函数、拷贝赋值运算符、析构函数等),但这些隐式生成的行为会被用户定义的函数 “抑制”。例如:

  • 如果用户定义了任何构造函数,编译器就不会再隐式生成默认构造函数(无参构造函数);
  • 如果用户定义了拷贝构造函数,编译器可能不会再隐式生成默认拷贝赋值运算符(视版本而定)。

default关键字的作用是:显式告诉编译器 “使用该函数的默认实现”,强制编译器生成符合标准的默认版本,避免手动编写冗余代码,同时保证编译器对默认函数的优化。

适用场景与示例:

  1. 恢复被抑制的默认构造函数当用户定义了带参构造函数时,默认构造函数会被抑制,此时可用default显式生成:

    class A 
    {public:A(int x) : _x(x) {}  // 用户定义了带参构造函数,默认构造被抑制A() = default;      // 显式要求编译器生成默认构造函数(无参)private:int _x;
    };
    
  2. 显式生成默认拷贝控制函数即使未定义其他拷贝函数,也可通过default明确使用默认版本,增强代码可读性:

    class B 
    {public:B(const B&) = default;        // 显式生成默认拷贝构造函数B& operator=(const B&) = default;  // 显式生成默认拷贝赋值运算符~B() = default;               // 显式生成默认析构函数(编译器通常会默认生成,但显式写出更清晰)
    };
    

    注意:default仅能用于编译器原本会隐式生成的特殊成员函数(如默认构造、拷贝构造、析构等),不能用于普通成员函数。

**delete:**显式禁止编译器生成特定的特殊成员函数(或普通函数)

在 C++98 中,若要禁止类的拷贝行为(如单例模式),通常的做法是 “将拷贝构造函数和拷贝赋值运算符声明为private且不实现”,但这种方式只能在链接期报错(未实现),且不直观。

delete关键字的作用是:显式告诉编译器 “禁止生成该函数”,若用户尝试使用被delete的函数,编译器会在编译期直接报错,更安全、更清晰。

适用场景与示例:

  1. 禁止类的拷贝行为对于不可拷贝的类(如std::unique_ptr),可通过delete删除拷贝构造和拷贝赋值:

    class Singleton 
    {public:// 禁止拷贝构造Singleton(const Singleton&) = delete;// 禁止拷贝赋值Singleton& operator=(const Singleton&) = delete;static Singleton& getInstance() {static Singleton instance;return instance;}private:Singleton() = default;  // 私有默认构造,确保只能通过getInstance获取实例
    };
    

    此时若尝试拷贝Singleton对象(如auto s = Singleton::getInstance();),编译器会直接报错。

  2. 禁止特定参数类型的函数重载(包括普通函数)delete不仅可用于特殊成员函数,还能用于普通函数,禁止特定参数的调用(如防止隐式类型转换):

    class MyInt 
    {public:MyInt(int x) : _x(x) {}// 禁止从double隐式转换为MyInt(只允许int转换)MyInt(double) = delete;  private:int _x;
    };int main() 
    {MyInt a(10);       // 正确:int转换MyInt b(3.14);     // 错误:double版本被delete,编译报错return 0;
    }
    

总结

  • default显式请求编译器生成默认版本的特殊成员函数,解决 “用户定义函数抑制默认函数” 的问题,代码更简洁且享受编译器优化。
  • delete显式禁止编译器生成特定函数(包括特殊成员函数和普通函数),编译期检查错误,比 C++98 的 “私有不实现” 更安全、直观,常用于禁止拷贝或特定类型转换。

5.3 其他

在C++11中对于类还有其他的功能,例如在成员变量声明时给缺省值,以及finaloverride等关键字,对于这些内容感兴趣的可以看博主之前的文章(直接点击上方链接),里面有详细讲解。

6. lambda表达式:匿名函数的优雅实现

在 C++11 前,要传递一个 “短小的函数逻辑”,要么写独立函数,要么写仿函数(需定义类)—— 这两种方式都很繁琐。而lambda 表达式(匿名函数对象)完美解决了这个问题,支持在函数内部直接定义可调用对象。

6.1 基本语法

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

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

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

下面是一个简单的lambda表达式:

int main()
{// ⼀个简单的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;auto swap1 = [](int& x, int& y){int tmp = x;x = y;y = tmp;};swap1(a, b);cout << a << ":" << b << endl;return 0;
}

6.2 捕捉列表

lambda 表达式中默认只能⽤ lambda 函数体内和参数中的变量,如果想用外层作⽤域中的变量就需要进行捕捉。一共有三种方法:

  • 第⼀种捕捉⽅式是在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。例如:[x, y, &z] 表⽰x和y是值捕捉,z是引用捕捉。 捕捉列表中的变量名就是想捕捉外层作用域的变量名。
  • 第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表⽰隐式值捕捉,在捕捉列表写⼀个&表⽰隐式引⽤捕捉,可以任意使用外层作用域中的变量,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些变量。
  • 第三种捕捉⽅式是在捕捉列表中混合使⽤隐式捕捉和显⽰捕捉。[=, &x]表⽰其他变量隐式值捕捉,而x是引⽤捕捉;[&, x, y]表⽰其他变量引⽤捕捉,x和y是值捕捉。当使⽤混合捕捉时,第⼀个元素必须是&=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉。

lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使⽤。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。

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

6.3 应用场景

在学习 lambda 表达式之前,我们使⽤的可调⽤对象只有函数指针和仿函数对象,函数指针的类型定义起来⽐较⿇烦,仿函数又要定义⼀个类,相对会⽐较⿇烦。使⽤ lambda 去定义可调⽤对象,既简单⼜⽅便。

lambda 在很多其他地⽅⽤起来也很好⽤。⽐如线程中定义线程的执⾏函数逻辑,智能指针中定制删除器等。这里我们举一个排序函数的例子:之前我们使用排序函数时如果要控制具体的排序方式需要传入仿函数,而当我们有了lambda表达式后就可以直接定义,不用再去写仿函数,如下面代码所示:

struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价// ...Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3}, { "菠萝", 1.5, 4 } };// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中// 不同项的⽐较,相对还是⽐较麻烦的,那么这⾥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;
}

6.4 原理

lambda 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for这样的东西。范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个lambda表达式以后,编译器会⽣成⼀个对应的仿函数的类。

仿函数的类名是编译器按⼀定规则⽣成的,保证不同的 lambda⽣成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是⽣成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda 类构造函数的实参,对于隐式捕捉,编译器要看使⽤了哪些变量,就传哪些变量。

lambda表达式看似 “匿名”,实则是编译器帮我们隐藏了类定义。

7. 包装器:统一可调用对象类型

C++ 中的可调用对象(函数指针、仿函数、lambda表达式、成员函数)类型各异,导致在存储或传递时非常不便。C++11 的包装器std::functionstd::bind)解决了这一问题。

7.1 function:统一可调用对象类型

std::function 是⼀个类模板,也是⼀个包装器。 std::function 的实例对象可以包装存储其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调⽤对象被称为 std::function 的⽬标。若 std::function 不含⽬标,则称它为空。调⽤空 std::function 的⽬标会导致抛出 std::bad_function_call 异常。

std::function 是⼀个类模板,也是⼀个包装器。 std::function 的实例对象可以包装存储其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调⽤对象被称为 std::function 的⽬标。若 std::function 不含⽬标,则称它为空。调⽤空 std::function 的⽬标导致抛出 std::bad_function_call 异常。

下面是 function 的原型,他被定义<functional>头⽂件中(官方文档):

template <class T>
class function;          // undefinedtemplate <class Ret, class... Args>
class function<Ret(Args...)>;

函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同, std::function 的优势就是统⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型。对于function类模板的参数,有特殊的语法:function<返回值(参数列表)>,如下面代码所示:

#include<functional>
//普通函数
int f(int a, int b)
{return a + b;
} 
//仿函数
struct Functor
{ public:int operator() (int a, int b){return a + b;}
};
//成员函数
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;}privateint _n;
};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;function<double(Plus, double, double)> f6 = &Plus::plusd;cout << f6(pd, 1.1, 1.1) << endl;cout << f6(pd, 1.1, 1.1) << endl;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;return 0;
}

std::function的典型场景是作为容器的值类型,例如在逆波兰表达式求值中用map实现 “字符串→可调用对象” 的映射:

// 使⽤map映射string和function的⽅式实现
// 这种⽅式的最⼤优势之⼀是⽅便扩展,假设还有其他运算,我们增加map中的映射即可
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();}
}

7.2 bind:调整可调用对象的参数

std::bind 是⼀个函数模板,它也是⼀个可调⽤对象的包装器,可以把他看做⼀个函数适配器,对接收的可调⽤对象进⾏处理后返回⼀个可调⽤对象。 bind 可以⽤来调整参数个数和参数顺序。bind 也在<functional>这个头⽂件中

下面是 bind 的定义:

simple(1)
template <class Fn, class... Args>/* unspecified */ bind (Fn&& fn, Args&&... args);
with return type (2)
template <class Ret, class Fn, class... Args>/* unspecified */ bind (Fn&& fn, Args&&... args);

调⽤bind的⼀般形式: auto newCallable = bind(callable,arg_list); 其中Callable本⾝是⼀个可调⽤对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的参数。当我们调⽤newCallable时,newCallable会调⽤callable,并传给它arg_list中的参数。

arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表⽰newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表⽰⽣成的可调⽤对象中参数的位置:_1newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3…这些占位符被放到placeholders的⼀个命名空间中。举一个简单的例子:

#include<iostream>
#include<functional>
using namespace std;using placeholders::_1;
using placeholders::_2;
using placeholders::_3;void Sub(int a, int b, int c)
{cout << a << ' ' << b << ' ' << c << endl;
}
int main()
{auto sub1 = bind(Sub, _2, _1, 3);sub1(1, 2);  // sun1中的第一个参数永远传给bind中的_1,在sub中,_1对应第二个参数,其他同理// 2 1 3auto sub2 = bind(Sub, _1, 3, _2);sub2(1, 2);//1 3 2return 0;
}

bind可以调整参数顺序,但是并不常用,它最常用的地方在于可以调整参数个数,也就是说可以绑定某些变量,不用每次都传,如下面代码所示:

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<double(Plus&&, double, double)> f1 = &Plus::plusd;Plus pd;cout << f1(move(pd), 1.1, 1.1) << endl;cout << f1(Plus(), 1.1, 1.1) << endl;// bind⼀般⽤于绑死⼀些固定参数function<double(double, double)> f2 = bind(&Plus::plusd, Plus(), _1, _2);cout << f2(1.1, 1.1) << endl;
}

8. 小结

C++11 并非简单的 “特性堆砌”,而是对 C++ 语言的一次 “重塑”:

  • 解决了历史痛点:用移动语义消除冗余拷贝,用列表初始化统一语法,用 lambda 简化匿名函数;
  • 奠定现代范式:引入可变参数模板、包装器等特性,为泛型编程提供更灵活的工具;
  • 推动生态发展:STL 的更新(如unordered_mapemplace系列)和多线程内存模型,让 C++ 更适应现代软件开发需求。

如果你至今仍在使用 C++98 的思维编写代码,不妨从今天开始,尝试用 C++11 的特性重构你的项目 —— 相信我,它会让你的代码更优雅、更高效。

尾声

若有纰漏或不足之处欢迎大家在评论区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!

http://www.dtcms.com/a/520145.html

相关文章:

  • 【unity】运行时加载并修改ScriptableObject类型资源对象的值会怎样
  • Spring Boot 实现 DOCX 转 PDF(基于 docx4j 的轻量级开源方案)
  • 服装企业官方网站建设网站运营收入
  • Spring Boot Actuator深度解析与实战
  • 如何做 行业社交类网站网站 建设在作用
  • 线程3 JavaEE(阻塞队列,线程池)
  • K8s中,deployment 是如何从 yaml 文件最终部署成功 pod 的
  • RK3588 使用 FFmpeg 硬件解码输出到 DRM Prime (DMA Buf) 加速数据传输
  • 基于蚁群算法的PID参数整定方法及MATLAB实现
  • 排序算法大全——插入排序
  • 手搓一个CUDA JIT编译器
  • 网站引导页模板互联网公司排名全球
  • JDK 9 List.of(...)
  • 做一个vue3 v-model 双向绑定的弹窗
  • 为超过10亿条记录的订单表新增字段
  • 哪里做网站最便宜WordPress功能模块排版
  • 每日算法刷题Day78:10.23:leetcode 一般树7道题,用时1h30min
  • 薄膜测厚选CWL法还是触针法?针对不同厚度与材质的台阶仪技术选型指南
  • WPF-MVVM的简单入门(第一个MVVM程序)
  • blender拓扑建模教程
  • asp.net手机网站开发教程翻译网站建设方案
  • 佛山建设网站公司哪家好特斯拉ceo进厂拧螺丝
  • 如何做新网站保留域名wordpress基础
  • C# 实现 Modbus TCP 通信
  • 《Git:从入门到精通(七)——Git分支管理与协作开发实战》
  • 超越传统工具:利用Reddit发现关键词的独特视角与前沿方法
  • 数据结构——二叉搜索树深度解析
  • macOS 无法在根目录创建目录的原因与解决方案
  • 11.23 鸿蒙HTTP数据请求
  • 郑州网站建设最低价网址导航的意思