C++版本发展史

2023-05-16

1. C++ 98

2. C++ 03

3. C++ 11

    3.1 nullptr

    3.2 auto

    3.3 decltype

    3.4 初始化列表

    3.5 范围for循环

    3.6 右值引用

    3.7 字符串字面量

    3.8 noexcept

    3.9 constexpr

    3.10 template特性

    3.11 Lambda表达式

    3.12 函数声明语法

    3.13 强类型枚举

    3.14 新增基础类型

    3.15 多线程Thread

    3.16 智能指针

    3.17 元组std::tuple

    3.18 新增容器

    3.19 构造函数

4. C++ 14

5. C++ 17

6. C++ 20

    6.1 模块 (Modules)

    6.2 协程 (Coroutines)

    6.3 范围 (Ranges)

    6.4 概念与约束 (Constraints and concepts)


     C++是一门以C为基础发展而来的一门面向对象的高级程序设计语言,从1983年由Bjarne Stroustrup教授在贝尔实验室创立开始至今,已有30多个年头。C++从最初的C with class,经历了从C++98、C++ 03、C++ 11、C++ 14、C++17再到C++ 20多次标准化改造,功能得到了极大的丰富,已经演变为一门集面向过程、面向对象、函数式、泛型和元编程等多种编程范式的复杂编程语言。

年份

C++ 标准名称

非正式名称

1998

ISO/IEC 14882:1998

C++98

2003

ISO/IEC 14882:2003

C++03

2011

ISO/IEC 14882:2011

C++11

2014

ISO/IEC 14882:2014

C++14

2017

ISO/IEC 14882:2017

C++17

2020

ISO/IEC 14882:2020

C++20

1. C++ 98

    1954年,John Backus发明了世界上第一种计算机高级语言Fortran,为之后出现的高级编程语言奠定了基础。

    1970年,AT&T的Bell实验室的 Ken Thompson,以BCPL语言为基础,设计出简单且接近硬件的B语言(取BCPL的首字母),并且他用B语言写了第一个Unix操作系统。

    1972年,Bell实验室的Dennis Ritchie和Ken Thompson共同发明了C语言,并使用C重写Unix。

    1979年,Bjame Stroustrup到了Bell实验室,开始从事将C改良为带类的C(C with Classes)的工作,1983年该语言被正式命名为C++,主要意图是表明C++是C的增强版。

    1985年发布了第一个C++版本。第一个版本的C++,因其面向对象的思想使得编程变得简单,并且又保持了C语言的运行效率,在推出的一段时间内,得到了快速的发展,占据了编程语言界的半壁江山。

    从1985年到1998年,C++从最初的C with Classes新增了很多其他的特性,比如异常处理、模板、标准模板库(STL)、运行时异常处理(RTTI)与名字空间(Namespace)等。

    1998年,C++标准委员会统筹C++的所有特性,发布了第一个C++国际标准C++98。

2. C++ 03

    从1998年到2003年,是C++标准从C++98到C++03的迭代期,期间C++扩增了很多额外的特性,比如以Boost MPL(Boost Metaprogramming Library)与Loki等为代表的模板元编程库的出现,让开发者更加便捷的使用C++在编译期的执行能力,即通过代码编译获得计算结果,学术性的称为模板元编程。到了2003年,C++标准委员会总结最新技术并发布了C++03标准。C++03 是给 C++98 打的补丁,所以现在的人提到 C++98, C++03 往往指的是同一个。

3. C++ 11

    从2003年到2011年,也就是从C++03到C++11,期间C++引入了对象移动、右值引用、lamba表达式(函数式编程)、编译时类型识别(auto)、别名模板以及很多新型关键词(如nullptr、decltype、constexpr)等现代编程语言常具备的能力,让C++与时俱进,开发效率得到了很大的提升。这些新的特性随着C++11标准的发布而被正式确立下来。C++ 11版本也被称为现代C++,而C++ 98/03版本也被称为传统C++。

3.1 nullptr

    实际开发中,避免产生“野指针”最有效的方法,就是在定义指针的同时完成初始化操作,即便该指针的指向尚未明确,也要将其初始化为空指针。所谓“野指针”,又称“悬挂指针”,指的是没有明确指向的指针。野指针往往指向的是那些不可用的内存区域,这就意味着像操作普通指针那样使用野指针(例如 &p),极可能导致程序发生异常。C++98/03 标准中,将一个指针初始化为空指针的方式有 2 种:

int *p = 0;
int *p = NULL; //推荐使用

    可以看到,我们可以将指针明确指向 0(0x0000 0000)这个内存空间。一方面,明确指针的指向可以避免其成为野指针;另一方面,大多数操作系统都不允许用户对地址为 0 的内存空间执行写操作,若用户在程序中尝试修改其内容,则程序运行会直接报错。相比第一种方式,我们更习惯将指针初始化为 NULL。值得一提的是,NULL 并不是 C++ 的关键字,它是 C++ 为我们事先定义好的一个宏,并且它的值往往就是字面量 0(#define NULL 0)。 C++ 中将 NULL 定义为字面常量 0,虽然能满足大部分场景的需要,但个别情况下,它会导致程序的运行和我们的预期不符

    由于 C++ 98 标准使用期间,NULL 已经得到了广泛的应用,出于兼容性的考虑,C++11 标准并没有对 NULL 的宏定义做任何修改。为了修正 C++ 存在的这一 BUG,C++ 标准委员会最终决定另其炉灶,在 C++11 标准中引入一个新关键字,即 nullptr。nullptr 是 nullptr_t 类型的右值常量,专用于初始化空类型指针。nullptr_t 是 C++11 新增加的数据类型,可称为“指针空值类型”。也就是说,nullpter 仅是该类型的一个实例对象(已经定义好,可以直接使用),如果需要我们完全定义出多个同 nullptr 完全一样的实例对象。nullptr 可以被隐式转换成任意的指针类型。举个例子:

int * a1 = nullptr;
char * a2 = nullptr;
double * a3 = nullptr;

3.2 auto

    在之前的 C++ 版本中,auto 关键字用来指明变量的存储类型,它和 static 关键字是相对的。auto 表示变量是自动存储的,这也是编译器的默认规则,所以写不写都一样,一般我们也不写,这使得 auto 关键字的存在变得非常鸡肋。C++11 赋予 auto 关键字新的含义,使用它来做自动类型推导。也就是说,使用了 auto 关键字以后,编译器会在编译期间自动推导出变量的类型,这样我们就不用手动指明变量的数据类型了。auto 关键字基本的使用语法如下:

auto name = value;
int  x = 0;
auto *p1 = &x;   //p1 为 int *,auto 推导为 int
auto  p2 = &x;   //p2 为 int*,auto 推导为 int*
auto &r1  = x;   //r1 为 int&,auto 推导为 int
auto r2 = r1;    //r2 为  int,auto 推导为 int

    name 是变量的名字,value 是变量的初始值。注意:auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代。或者说,C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。

    auto 的限制

  • 使用 auto 的时候必须对变量进行初始化
  • auto 不能在函数的参数中使用
  • auto 不能作用于类的非静态成员变量
  • auto 关键字不能定义数组,如比如下面的例子就是错误的:
char url[] = "http://c.biancheng.net/";
auto  str[] = url;  //arr 为数组,所以不能使用 auto
  • auto 不能作用于模板参数 

3.3 decltype

    decltype 是 C++ 11新增的一个关键字,它和 auto 的功能一样,都用来在编译时期进行自动类型推导。既然已经有了 auto 关键字,为什么还需要 decltype 关键字呢?因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来非常不方便,甚至压根无法使用,所以 decltype 关键字也被引入到 C++11 中。auto 和 decltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的:

auto varname = value;
decltype(exp) varname = value;

    其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。auto 根据=右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。另外,auto 要求变量必须初始化,而 decltype 不要求。这很容易理解,auto 是根据变量的初始值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。decltype 可以写成下面的形式: 

decltype(exp) varname;

    原则上讲,exp 就是一个普通的表达式,它可以是任意复杂的形式,但是我们必须要保证 exp 的结果是有类型的,不能是 void;例如,当 exp 调用一个返回值类型为 void 的函数时,exp 的结果也是 void 类型,此时就会导致编译错误。C++ decltype 用法举例: 

int a = 0;
decltype(a) b = 1;  //b 被推导成了 int
decltype(10.8) x = 5.5;  //x 被推导成了 double
decltype(x + 100) y;  //y 被推导成了 double

 3.4 初始化列表

(1)一致性初始化

    在 C++ 98/03 中的对象初始化方法有很多种,包括小括号,大括号和赋值操作符,这些不同的初始化方法,都有各自的适用范围和作用。最关键的是,这些种类繁多的初始化方法,没有一种可以通用所有情况。为了统一初始化方式,并且让初始化行为具有确定的效果,C++11 引入了“一致性初始化”的概念,意思是对任何初始化动作,你可以使用相同的语法,也就是使用大括号。

int values[]{1, 2, 3};
std::vector<int> v {2, 3, 5, 7, 11, 13, 17};
std::vector<std::string> cities {"bejing", "shanghai", "guangzhou", "shenzhen"};

(2)初始列

    初值列会强迫造成所谓的value initialization,意思是即使某个局部变量属于某个基础类型,也会被初始化为0或者nullptr(如果它是个指针的话):

int i;     // i是随机值
int j{};   // j初始化为0
int* p;    // p是未定义值
int* q{};  // q初始化为nullptr

3.5 范围for循环

    C++ 11标准之前(C++ 98/03 标准),如果要用 for 循环语句遍历一个数组或者容器,只能套用如下结构:

for(表达式 1; 表达式 2; 表达式 3){
    //循环体
}

    C++ 11 标准中,除了可以沿用前面介绍的用法外,还为 for 循环添加了一种全新的语法格式,如下所示:

for (declaration : expression){
    //循环体
}

     其中,两个参数各自的含义如下:

  • declaration:表示此处要定义一个变量,该变量的类型为要遍历序列中存储元素的类型。需要注意的是,C++ 11 标准中,declaration参数处定义的变量类型可以用 auto 关键字表示,该关键字可以使编译器自行推导该变量的数据类型。
  • expression:表示要遍历的序列,常见的可以为事先定义好的普通数组或者容器,还可以是用 {} 大括号初始化的序列。

3.6 右值引用

(1)左值和右值

    在 C++ 或者 C 语言中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式。确切的说 C++ 中左值和右值的概念是从 C 语言继承过来的。左值的英文简写为“lvalue”,右值的英文简写为“rvalue”。很多人认为它们分别是"left value"、"right value" 的缩写,其实不然。lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 "read value",指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。

    通常情况下,判断某个表达式是左值还是右值,最常用的有以下 2 种方法:

  • 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。
  • 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。

(2)右值引用

    C++98/03 标准中就有引用,使用 "&" 表示。但此种引用方式有一个缺陷,即正常情况下只能操作 C++ 中的左值,无法对右值添加引用。举个例子:

int num = 10;
int &b = num; //正确
int &c = 10; //错误

    如上所示,编译器允许我们为 num 左值建立一个引用,但不可以为 10 这个右值建立引用。因此,C++98/03 标准中的引用又称为左值引用。为此,C++11 标准新引入了另一种引用方式,称为右值引用,用 "&&" 表示。和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:

int num = 10;
int && a = num;  // error,右值引用不能初始化为左值
int && a = 10;

(3)移动构造函数

    在 C++ 11 标准之前(C++ 98/03 标准中),如果想用其它对象初始化一个同类的新对象,只能借助类中的复制(拷贝)构造函数。拷贝构造函数的实现原理很简单,就是为新对象复制一份和其它对象一模一样的数据。

#include <iostream>
using namespace std;
class demo{
public:
   demo():num(new int(0))
   {
      cout<<"construct!"<<endl;
   }
   //拷贝构造函数
   demo(const demo &d):num(new int(*d.num))
   {
      cout<<"copy construct!"<<endl;
   }
   ~demo()
   {
      cout<<"class destruct!"<<endl;
   }
private:
   int *num;
};
demo get_demo()
{
    return demo();
}
int main(){
    demo a = get_demo();
    return 0;
}

    可以看到,程序中定义了一个可返回 demo 对象的 get_demo() 函数,用于在 main() 主函数中初始化 a 对象,其整个初始化的流程包含以下几个阶段:

  1)执行 get_demo() 函数内部的 demo() 语句,即调用 demo 类的默认构造函数生成一个匿名对象;

  2)执行 return demo() 语句,会调用拷贝构造函数复制一份之前生成的匿名对象,并将其作为 get_demo() 函数的返回值(函数体执行完毕之前,匿名对象会被析构销毁);

  3)执行 a = get_demo() 语句,再调用一次拷贝构造函数,将之前拷贝得到的临时对象复制给 a(此行代码执行完毕,get_demo() 函数返回的对象会被析构);

  4)程序执行结束前,会自行调用 demo 类的析构函数销毁 a。

    完整的输出结果如下:

construct!                <-- 执行 demo()
copy construct!       <-- 执行 return demo()
class destruct!         <-- 销毁 demo() 产生的匿名对象
copy construct!       <-- 执行 a = get_demo()
class destruct!         <-- 销毁 get_demo() 返回的临时对象
class destruct!         <-- 销毁 a

     如上所示,利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次拷贝(而且是深拷贝)操作。当然,对于仅申请少量堆空间的临时对象来说,深拷贝的执行效率依旧可以接受,但如果临时对象中的指针成员申请了大量的堆空间,那么 2 次深拷贝操作势必会影响 a 对象初始化的执行效率。

    所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。以前面程序中的 demo 类为例,该类的成员都包含一个整形的指针成员,其默认指向的是容纳一个整形变量的堆空间。当使用 get_demo() 函数返回的临时对象初始化 a 时,我们只需要将临时对象的 num 指针直接浅拷贝给 a.num,然后修改该临时对象中 num 指针的指向(通常另其指向 NULL),这样就完成了 a.num 的初始化。

#include <iostream>
using namespace std;
class demo{
public:
    demo():num(new int(0))
    {
        cout<<"construct!"<<endl;
    }
    demo(const demo &d):num(new int(*d.num))
    {
        cout<<"copy construct!"<<endl;
    }
    //添加移动构造函数
    demo(demo &&d):num(d.num)
    {
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }
    ~demo(){
        cout<<"class destruct!"<<endl;
    }
private:
    int *num;
};
demo get_demo()
{
    return demo();
}
int main()
{
    demo a = get_demo();
    return 0;
}

    可以看到,在之前 demo 类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。 命令执行此程序,输出结果为:

construct!
move construct!
class destruct!
move construct!
class destruct!
class destruct!

    通过执行结果我们不难得知,当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。我们知道,非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。 

(4)move语义

    C++11 标准中借助右值引用可以为指定类添加移动构造函数,这样当使用该类的右值对象(可以理解为临时对象)初始化同类对象时,编译器会优先选择移动构造函数。注意,移动构造函数的调用时机是:用同类的右值对象初始化新对象。那么,用当前类的左值对象(有名称,能获取其存储地址的实例对象)初始化同类对象时,是否就无法调用移动构造函数了呢?当然不是,C++11 标准中已经给出了解决方案,即调用 move() 函数。move 本意为 "移动",但该函数并不能移动任何数据,它的功能很简单,就是将某个左值强制转化为右值。move() 函数的用法也很简单,其语法格式如下:

move( arg )

(5)完美转发

    完美转发指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。举个例子:

template<typename T>
void function(T t) {
    otherdef(t);
}

    如上所示,function() 函数模板中调用了 otherdef() 函数。在此基础上,完美转发指的是:如果 function() 函数接收到的参数 t 为左值,那么该函数传递给 otherdef() 的参数 t 也是左值;反之如果 function() 函数接收到的参数 t 为右值,那么传递给 otherdef() 函数的参数 t 也必须为右值。显然,function() 函数模板并没有实现完美转发。一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;另一方面,无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。总之,无论从那个角度看,function() 函数的定义都不“完美”。 

    C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。

#include <iostream>
using namespace std;
//重载被调用函数,查看完美转发的效果
void otherdef(int & t) 
{
    cout << "lvalue\n";
}
void otherdef(const int & t) 
{
    cout << "rvalue\n";
}
//实现完美转发的函数模板
template <typename T>
void function(T&& t) 
{
    otherdef(forward<T>(t));
}
int main()
{
    function(5);
    int  x = 1;
    function(x);
    return 0;
}

    程序执行结果为:

rvalue
lvalue

3.7 字符串字面量

    想象一下如下场景,我们要打印如下内容:

this is "test"

    我们不得不用如下的代码,对" 进行转义:

std::string normal_str = "this is \"test\"";

     C++11引入了字符串字面量的概念。对于前面的例子,我们就可以通过如下的方式实现我们的目的:

std::string normal_str = R"(this is "test")";

    C++11支持用户自定义字面量,这里就不再赘述,感兴趣的自行百度。 

3.8 noexcept

    C++11新标准引入的noexcept运算符,可以用于指定某个函数不抛出异常。预先知道函数不会抛出异常有助于简化调用该函数的代码,而且编译器确认函数不会抛出异常,它就能执行某些特殊的优化操作。C++ 98/03版本中常用throw()表示,在C++ 11中已经被noexcept代替。

(1)noexcept异常说明

  C++ 98/03版本:

void func(int x) throw();  //不抛出异常

  C++ 11版本:

void func(int x) noexcept;  //不抛出异常
void func1(int x);  //抛出异常

    对于程序违反了异常说明,编译器在编译阶段不会检查报错,但是在程序执行过程中,程序会调用terminate以确保遵守不在运行时抛出异常的承诺。 

void func() noexcept
{
  throw exception();
}
  •   对于一个函数来说,noexcept说明要么出现在该函数的所有生命语句和定义语句中,要么一次也不出现。
  • 可以再函数指针的声明和定义中指定noexcept。
  • 在typedef和类型别名中不可以出现noexcept。
  • 在成员函数中,noexcept需要跟在const以及引用限定符之后,在final、override或虚函数=0之前。

(2)noexcept运算符

    noexcept运算符是一个一元运算符,它的返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。

noexcept(f());   //如果f()不抛出异常则结果为true,否则为false
noexcept(e);     //当e调用的所有函数都做了步抛出说明且e本身不含有throw语句时,表达式为true,否则返回false

3.9 constexpr

    常量表达式,指的就是由多个(≥1)常量组成的表达式。换句话说,如果表达式中的成员都是常量,那么该表达式就是一个常量表达式。这也意味着,常量表达式一旦确定,其值将无法修改。我们知道,C++ 程序的执行过程大致要经历编译、链接、运行这 3 个阶段。值得一提的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果;而常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。对于用 C++ 编写的程序,性能往往是永恒的追求。那么在实际开发中,如何才能判定一个表达式是否为常量表达式,进而获得在编译阶段即可执行的“特权”呢?除了人为判定外,C++11 标准还提供有 constexpr 关键字。
    constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。 C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。

#include <iostream>
using namespace std;
int main()
{
    constexpr int num = 1 + 2 + 3;
    int url[num] = {1,2,3,4,5,6};
    couts<< url[1] << endl;
    return 0;
}

3.10 template特性

    类模板:通用的类描述(使用泛型来定义类),进行实例化时,其中的泛型再用具体的类型替换。

    函数模板:通用的函数描述(使用泛型来定义函数),进行实例化时,其中的泛型再用具体的类型替换。

(1)C++98标准中两者的区别

    函数模板和类模板在C++98标准中一起被引入,两者区别主要在于:在类模板声明时,标准允许其有默认模板参数。而函数模板却不支持。默认模板参数的作用如同函数的默认形参。不过在C++11中,这一限制已经被解除了,如下例所示:

void DefParm(int m = 3) {} // c++98编译通过,c++11编译通过

template <typename T = int>
class DefClass {};        // c++98编译通过,c++11编译通过

template <typename T = int>
void DefTempParm() {};    // c++98编译失败,c++11编译通过

    可以看到,DefTempParm函数模板拥有一个默认模板参数(类型int)。使用仅支持C++98的编译器编译,DefTempParm的编译会失败,而支持C++11的编译器则无问题。

(2)C++11标准中两者的区别

    尽管C++11支持了函数模板的默认模板参数,不过在语法上,两者还是存在区别:类模板在为多个默认模板参数声明指定默认值时,必须遵照“从右往左”的规则进行指定。而这个规则对函数模板来说并不是必须的。示例如下:

template <typename T1, typename T2 = int>
class DefClass1 {};

template <typename T1 = int, typename T2>
class DefClass2 {};  // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则

template <typename T, int i = 0>
class DefClass3 {};

template <int i = 0, typename T>
class DefClass4 {};  // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则

template <typename T1 = int, typename T2>
void DefFunc1(T1 a, T2 b) {}; // OK 函数模板不用遵循“由右往左”的规则

template <int i = 0, typename T>
void DefFunc2(T a) {};  // OK 函数模板不用遵循“由右往左”的规则

    可以看到,不按照从右往左定义默认类模板参数的模板类DefClass2和DefClass4都无法通过编译。而对于函数模板来说,默认模板参数的位置则比较随意。DefFunc1和DefFunc2都为第一个模板参数定义了默认参数,而第二个模板参数的默认值并没有定义,C++11编译器却认为没有问题。

3.11 Lambda表达式

    lambda 源自希腊字母表中第 11 位的 λ,在计算机科学领域,它则是被用来表示一种匿名函数。所谓匿名函数,简单地理解就是没有名称的函数,又常被称为 lambda 函数或者 lambda 表达式。

(1)匿名函数定义

    定义一个 lambda 匿名函数很简单,可以套用如下的语法格式:

[外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型
{
   函数体;
};
  • [外部变量方位方式说明符]:[ ] 方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略。在方括号内部,可以注明当前 lambda 函数的函数体中可以使用哪些“外部变量”。

    所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量。

  • (参数):和普通函数的定义一样,lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略;

  • mutable:此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字。

  • noexcept/throw():可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。值得一提的是,如果 lambda 函数标有 noexcept 而函数体内抛出了异常,又或者使用 throw() 限定了异常类型而函数体内抛出了非指定类型的异常,这些异常无法使用 try-catch 捕获,会导致程序执行失败。

  • -> 返回值类型:指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略-> 返回值类型

  • 函数体:和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。需要注意的是,外部变量会受到以值传递还是以引用传递方式引入的影响,而全局变量则不会。换句话说,在 lambda 表达式内可以使用任意一个全局变量,必要时还可以直接修改它们的值。

(2)匿名函数中的[外部变量 

    对于 lambda 匿名函数的使用,比较让人感到困惑的就是 [外部变量] 的使用。其实很简单,无非下表所示的这几种编写格式。

 
外部变量格式功能
[]空方括号表示当前 lambda 匿名函数中不导入任何外部变量。
[=]只有一个 = 等号,表示以值传递的方式导入所有外部变量;
[&]只有一个 & 符号,表示以引用传递的方式导入所有外部变量;
[val1,val2,...]表示以值传递的方式导入 val1、val2 等指定的外部变量,同时多个变量之间没有先后次序;
[&val1,&val2,...]表示以引用传递的方式导入 val1、val2等指定的外部变量,多个变量之间没有前后次序;
[val,&val2,...]以上 2 种方式还可以混合使用,变量之间没有前后次序。
[=,&val1,...]表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。
[this]表示以值传递的方式导入当前的 this 指针。

     注意:单个外部变量不允许以相同的传递方式导入多次。例如 [=,val1] 中,val1 先后被以值传递的方式导入了 2 次,这是非法的。

(3)使用实例

#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
    int num[4] = {4, 2, 3, 1};
    //对 a 数组中的元素进行排序
    sort(num, num + 4, [=](int x, int y) -> bool{ return x < y; } );
    for(int n : num)
    {
        cout << n << " ";
    }
    return 0;
}

    程序执行结果为:1 2 3 4。调用 sort() 函数实现了对 num 数组中元素的升序排序,其中就用到了 lambda 匿名函数。而如果使用普通函数,需以如下代码实现:

#include <iostream>
#include <algorithm>
using namespace std;
//自定义的升序排序规则

bool sort_up(int x, int y)
{
    return  x < y;
}

int main()
{
    int num[4] = {4, 2, 3, 1};
    //对 a 数组中的元素进行排序
    sort(num, num+4, sort_up);
    for (int n : num)
    {
        cout << n << " ";
    }
    return 0;
}

    此程序中 sort_up() 函数的功能和上一个程序中的 lambda 匿名函数完全相同。显然在类似的场景中,使用 lambda 匿名函数更有优势。除此之外,虽然 lambda 匿名函数没有函数名称,但我们仍可以为其手动设置一个名称,比如:

#include <iostream>
using namespace std;
int main()
{
    //display 即为 lambda 匿名函数的函数名
    auto display = [](int a,int b) -> void{cout << a << " " << b;};
    //调用 lambda 函数
    display(10,20);
    return 0;
}

     程序执行结果为:10 20。可以看到,程序中使用 auto 关键字为 lambda 匿名函数设定了一个函数名,由此我们即可在作用域内调用该函数。

3.12 函数声明语法

    在C++11中,callable object 包括传统C函数,C++成员函数,函数对象(实现了()运算符的类的实例),lambda表达式(特殊函数对象)共4种。程序设计,特别是程序库设计时,经常需要涉及到回调,如果针对每种不同的callable object单独进行声明类型,代码将会非常散乱,也不灵活。如下示例:

// 传统C函数
int c_function(int a, int b)
{
    return a + b;
}

// 函数对象
class Functor
{
public:
    int operator()(int a, int b)
    {
        return a + b;
    }
};

int main(int argc, char** argv)
{
    int(*f)(int, int);    // 声明函数类型,赋值只能是函数指针
    f = c_function;
    cout << f(3, 4) << endl;

    Functor ff = Functor(); // 声明函数对象类型,赋值只能是函数对象
    cout << ff(3, 4) << endl;
}

    幸运的是,C++标准库的头文件里定义了std::function<>模板,此模板可以容纳所有类型的callable object.示例代码如下:

#include <iostream>
#include <functional>
using namespace std;

// 传统C函数
int c_function(int a, int b)
{
    return a + b;
}

// 函数对象
class Functor
{
public:
    int operator()(int a, int b)
    {
        return a + b;
    }
};

int main(int argc, char** argv)
{
    // 万能可调用对象类型
    std::function<int(int, int)> callableObject;

    // 可以赋值为传统C函数指针
    callableObject = c_function;
    cout << callableObject(3, 4) << endl;

    // 可以赋值为函数对象
    Functor functor;
    callableObject = functor;
    cout << callableObject(3, 4) << endl;

    // 可以赋值为lambda表达式(特殊函数对象)
    callableObject = [](int a, int b){
        return a + b;
    };
    cout << callableObject(3, 4) << endl;
}

3.13 强类型枚举

(1)传统枚举类型的缺陷

    枚举类型是C/C++中用户自定义的构造类型,它是由用户定义的若干枚举常量的集合。枚举值对应整型数值,默认从0开始。比如定义一个描述性别的枚举类型。

enum Gender{Male,Female};

    其中枚举值Male被编译器默认赋值为0,Female赋值为1。传统枚举类型在设计上会存在以下几个问题。

  • 同作用域同名枚举值会报重定义错误。传统C++中枚举常量被暴漏在同一层作用域中,如果同一作用域下有两个不同的枚举类型,但含有同名的枚举常量也是会报编译错误的,比如:
enum Fruits{Apple, Tomato, Orange};
enum Vegetables{Cucumber, Tomato, Pepper}; //编译报Tomato重定义错误

    其中水果和蔬菜两个枚举类型中包含同名的Tomato枚举常量会导致编译错误。因为enum则是非强作用域类型,枚举常量可以直接访问,这种访问方式与C++中具名的namespace、class/struct以及union必须通过"名字::成员名"的访问方式大相径庭。 

  • 由于枚举类型被设计为常量数值的“别名”,所以枚举常量总是可以被隐式转换为整型,且用户无法为枚举常量定义类型。
  • 枚举常量占用存储空间以及符号性不确定。C++标准规定C++枚举所基于的“基础类型”是由编译器来具体实现,这会导致枚举类型成员的基本类型存在不确定性问题,尤其是符号性问题, 。

(2)强类型枚举

    非强作用域类型,允许隐式转换为整型,枚举常量占用存储空间以及符号性的不确定,都是枚举类缺点。针对这些缺点,C++11引入了一种新的枚举类型——强类型枚举(strong-typed enum)。强类型枚举使用enum class语法来声明:

enum class Enumeration{VAL1, VAL2, VAL3 = 100, VAL4};

    强类型枚举具有如下几个优点:

  • 强作用域,强类型枚举成员的名称不会被输出到其父作用域,所以不同枚举类型定义同名枚举成员编译不会报重定义错误。进而使用枚举类型的枚举成员时,必须指明所属范围,比如Enum::VAL1,而单独的VAL1则不再具有意义;
  • 转换限制,强类型枚举成员的值不可以与整型发生隐式相互转换。比如比如Enumeration::VAL4==10;会触发编译错误;
  • 可以指定底层类型。强类型枚举默认的底层类型是int,但也可以显示地指定底层类型。具体方法是在枚举名称后面加上":type",其中type可以是除wchar_t以外的任何整型。比如: 
enum class Type:char{Low,Middle,High};

    注意:

  • 声明强类型枚举的时候,既可以使用关键字enum class,也可以使用enum struct。事实上,enum struct与enum class在语法上没有任何区别。 
  • 由于强类型枚举是强类型作用域的,故匿名的enum class可能什么都做不了,如下代码会报编译错误:
enum class {General, Light, Medium, Heavy} weapon;
int main()
{
    weapon = Medium; //编译出错
    bool b = weapon == weapon::Medium; //编译出错
    return 0;
}

3.14 新增基础类型

(1)C++ 03中的基本算术类型包括9种,列举如下:

(2)C++ 11中的基本算术类型包括12种,C++11的基本类型完全包含上述9种类型,除此之外还包括:

    C++11标准中的char16_t和char32_t用来处理Unicode字符,char16_t可以作为UTF-16的一个处理单元,char32_t可以作为UTF-32编码的一个处理单元。 

3.15 多线程Thread

    C++11新标准中引入五个头文件支持多线程编程,他们分别是:<thread>  <atomic>  <mutex>  <condition_variable>   <future>

(1)<thread>头文件:该头文件主要声明了 std::thread类,另外std::this_thread命名空间也在改头文件中。

(2)<atomic>头文件:该头文件主要声明了std::atomic和std::atomic_flag两个类,另外还申明了一套C风格的原子类型与C兼容的原子操作的函数。

(3)<mutex>头文件:该头文件主要声明了与互斥量(mutex)相关的类,包括std::mutex系列类、std::lock_guard类std::unique_lock类等。

(4)<condition_variable>头文件:该头文件主要声明了与条件变量相关的类,包括 std::condition_variable和std::condition_variable_any两个类。

(5)<future>头文件:该头文件主要声明了:

  • Futures类:std::future, shared_future
  • Providers类:std::promise, std::package_task
  • Providers函数:std::async()

3.16 智能指针

    所谓智能指针,可以从字面上理解为“智能”的指针。具体来讲,智能指针和普通指针的用法是相似的,不同之处在于,智能指针可以在适当时机自动释放分配的内存。也就是说,使用智能指针可以很好地避免“忘记释放内存而导致内存泄漏”问题出现。由此可见,C++ 也逐渐开始支持垃圾回收机制了,尽管目前支持程度还有限。C++98/03 标准中,支持使用 auto_ptr 智能指针来实现堆内存的自动回收;C++11 新标准在废弃 auto_ptr 的同时,增添了 unique_ptr、shared_ptr 以及 weak_ptr 这 3 个智能指针来实现堆内存的自动回收。

(1)shared_ptr

    和 unique_ptr、weak_ptr 不同之处在于,多个 shared_ptr 智能指针可以共同使用同一块堆内存。并且,由于该类型智能指针在实现上采用的是引用计数机制,即便有一个 shared_ptr 指针放弃了堆内存的“使用权”(引用计数减 1),也不会影响其他指向同一堆内存的 shared_ptr 指针(只有引用计数为 0 时,堆内存才会被自动释放)。shared_ptr<T> 类模板中,提供了多种实用的构造函数:

std::shared_ptr<int> p1;             //不传入任何实参
std::shared_ptr<int> p2(nullptr);    //传入空指针 nullptr
std::shared_ptr<int> p3(new int(10)); // 在构建 shared_ptr 智能指针,也可以明确其指向。
std::shared_ptr<int> p3 = std::make_shared<int>(10); // C++11 标准中还提供了 std::make_shared<T> 模板函数

// 调用拷贝构造函数
std::shared_ptr<int> p4(p3); 
std::shared_ptr<int> p4 = p3;

// 调用移动构造函数
std::shared_ptr<int> p5(std::move(p4)); 
std::shared_ptr<int> p5 = std::move(p4);

    同一普通指针不能同时为多个 shared_ptr 对象赋值,否则会导致程序发生异常。例如:

int* ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2(ptr); // 错误

 (2)unique_ptr

    作为智能指针的一种,unique_ptr 指针自然也具备“在适当时机自动释放堆内存空间”的能力。和 shared_ptr 指针最大的不同之处在于,unique_ptr 指针指向的堆内存无法同其它 unique_ptr 共享,也就是说,每个 unique_ptr 指针都独自拥有对其所指堆内存空间的所有权。这也就意味着,每个 unique_ptr 指针指向的堆内存空间的引用计数,都只能为 1,一旦该 unique_ptr 指针放弃对所指堆内存空间的所有权,则该空间会被立即释放回收。

std::unique_ptr<int> p1();         // 创建出空的 unique_ptr 指针:
std::unique_ptr<int> p2(nullptr);  // 创建出空的 unique_ptr 指针:
std::unique_ptr<int> p3(new int);  // 创建出了一个 p3 智能指针,其指向的是可容纳 1 个整数的堆存储空间。

// 基于 unique_ptr 类型指针不共享各自拥有的堆内存,因此 C++11 标准中的 unique_ptr 模板类没有提供拷贝构造函数,只提供了移动构造函数
std::unique_ptr<int> p4(new int);
std::unique_ptr<int> p5(p4);// 错误,堆内存不共享
std::unique_ptr<int> p5(std::move(p4)); // 正确,调用移动构造函数

(3)weak_ptr

    需要注意的是,C++11标准虽然将 weak_ptr 定位为智能指针的一种,但该类型指针通常不单独使用(没有实际用处),只能和 shared_ptr 类型指针搭配使用。甚至于,我们可以将 weak_ptr 类型指针视为 shared_ptr 指针的一种辅助工具,借助 weak_ptr 类型指针, 我们可以获取 shared_ptr 指针的一些状态信息,比如有多少指向相同的 shared_ptr 指针、shared_ptr 指针指向的堆内存是否已经被释放等等。

    需要注意的是,当 weak_ptr 类型指针的指向和某一 shared_ptr 指针相同时,weak_ptr 指针并不会使所指堆内存的引用计数加 1;同样,当 weak_ptr 指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。也就是说,weak_ptr 类型指针并不会影响所指堆内存空间的引用计数。除此之外,weak_ptr<T> 模板类中没有重载 * 和 -> 运算符,这也就意味着,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它。

std::weak_ptr<int> wp1; // 可以创建一个空 weak_ptr 指针
std::weak_ptr<int> wp2 (wp1); // 凭借已有的 weak_ptr 指针,可以创建一个新的 weak_ptr 指针

// 利用已有的 shared_ptr 指针为其初始化
std::shared_ptr<int> sp (new int);
std::weak_ptr<int> wp3 (sp);

3.17 元组std::tuple

    std::tuple是类似pair的模板。每个pair的成员类型都不相同,但每个pair都恰好有两个成员。std::tuple:成员类型不同,有任意数量的成员。使用方式如下:

(1)tuple的创建

  • 创建一个空的元组, 创建时,需要指定元组的数据类型
std::tuple<int, float, double, long, long long> first;
  • 创建一个元组并初始化元组
std::string str_second_1("_1");
std::string str_second_2("_2");

// 指定了元素类型为引用 和 std::string, 下面两种方式都是可以的,只不过第二个参数不同而已
std::tuple<std::string, std::string> second_1(str_second_1, std::string("_2"));
std::tuple<std::string, std::string> second_2(str_second_1, str_second_2);
  • 使用make_tuple创建元组
int i_fourth_1 = 4;
int i_fourth_2 = 44;
// 下面的两种方式都可以
std::tuple<int, int> forth_1    = std::make_tuple(i_fourth_1, i_fourth_2);
auto forth_2                    = std::make_tuple(i_fourth_1, i_fourth_2);

(2)tuple的遍历


#include <iostream> // std::cout
#include <tuple> // std::tuple, std::tuple_size
  
int main ()
{
 std::tuple<int, char, double> mytuple (10, 'a', 3.14);

 // tuple的大小
 std::cout << "mytuple has ";
 std::cout << std::tuple_size<decltype(mytuple)>::value;
 std::cout << " elements." << '\n';

 // 获取tuple的元素类型
 std::tuple_element<0, decltype(mytuple)>::type ages; // ages就为int类型
  
 // 获取元素
 std::cout << "the elements is: ";
 std::cout << std::get<0>(mytuple) << " ";
 std::cout << std::get<1>(mytuple) << " ";
 std::cout << std::get<2>(mytuple) << " ";
  
 std::cout << '\n';
  
 return 0;
}
  
//输出结果:
mytuple has 3 elements.
the elements is: 10 a 3.14

3.18 新增容器

(1)std::array

    array就是数组,为什么会出现这样一个容器呢,不是有vector和传统数组吗?那你有没有某些时候抱怨过vector速度太慢。array 保存在栈内存中,相比堆内存中的vector,我们就能够灵活的访问元素,获得更高的性能;同时真是由于其堆内存存储的特性,有些时候我们还需要自己负责释放这些资源。array就是介于传统数组和vector两者之间的容器,封装了一些函数,比传统数组方便,但是又没必要使用vector;array 会在编译时创建一个固定大小的数组array 不能够被隐式的转换成指针,定义时需要指定类型和大小。支持快速随机访问。不能添加或删除元素。 

(2)std::forward_list

    forward_list 是一个列表容器,使用方法和 list 基本类似。但forward_list 使用单向链表进行实现,提供了 O(1) 复杂度的元素插入,不支持快速随机访问,也是标准库容器中唯一一个不提供 size() 方法的容器。当不需要双向迭代时,具有比 list 更高的空间利用率。

(3)std::unordered_map/std::unordered_multimap、std::unordered_set/std::unordered_multiset

    加了个unordered前缀,也是把Hash正式带入了STL中,内部没有红黑树,无法自动排序,只是用Hash建立了映射,其他用法相同,当题目只需要映射而不要排序时候,用这个会快很多。

3.19 构造函数

(1)继承构造函数

     子类为完成基类初始化,在C++11之前,需要在初始化列表调用基类的构造函数,从而完成构造函数的传递。如果基类拥有多个构造函数,那么子类也需要实现多个与基类构造函数对应的构造函数。

class Base {
public:
	Base(int v): _value(v), _c(‘0’){}
	Base(char c): _value(0), _c(c){}
private:
	int _value;
	char _c;
};

class Derived: public Base {
public:
	// 初始化基类需要透传参数至基类的各个构造函数,非常麻烦
	Derived(int v) :Base(v) {}
	Derived(char c) :Base(c) {}

	// 假设派生类只是添加了一个普通的函数
	void display() {
		// dosomething		
	}
};

    书写多个派生类构造函数只为传递参数完成基类初始化,这种方式无疑给开发人员带来麻烦,降低了编码效率。从 C++11 开始,推出了继承构造函数(Inheriting Constructor),使用 using 来声明继承基类的构造函数,我们可以这样书写。

class Base {
public:
	Base(int v) :_value(v), _c('0'){}
	Base(char c): _value(0), _c(c){}
private:
	int _value;
	char _c;
};

class Derived :public Base {
public:
	// 使用继承构造函数
	using Base::Base;

	// 假设派生类只是添加了一个普通的函数
	void display() {
		//dosomething		
	}
};

    注意事项:

  • 继承构造函数无法初始化派生类数据成员。继承构造函数的功能是初始化基类,对于派生类数据成员的初始化则无能为力。 
  • 构造函数拥有默认值会产生多个构造函数版本,且继承构造函数无法继承基类构造函数的默认参数,所以我们在使用有默认参数构造函数的基类时就必须要小心。
  • 多继承的情况下,继承构造函数会出现“冲突”的情况,因为多个基类中的部分构造函数可能导致派生类中的继承构造函数的函数名与参数相同,即函数签名。

(2)委托构造函数

    在实际的开发中,为了满足不同用户的不同需求,我们的一个类可能会有很多构造函数的重载版本,特别的,这些重载版本的工作内容有的比较复杂,有的比较简单,并且他们之间会有一些交叉重复的工作,即一些代码、或者数据的初始化在每个构造函数中都会去写一遍,这样显得代码特别的臃肿丑陋,委托构造函数应运而生。使用委托构造函数实例:允许在一个构造函数的初始化列表中调用另外一个构造函数,委托另外一个构造函数进行一些初始化工作。

  • 在没法进行委托构造时我们的code可能是这样的:
class foo {
public:
    foo(int data) :ma(data) {}
    foo(int data, int a) {
        ma = data + a;
        cout << "foo(int,int)" << endl;
    }
    foo(int data, int a, int b) :foo(data, a) {
        ma = data + a;
        ma += b;
        cout << "foo(int,int,int)" << endl;
    }
    foo(int data, int a, int b, int c) :foo(data, a, b) {
        ma = data + a;
        ma += b;
        ma += c;
        cout << "foo(int,int,int,int)" << endl;
    }
    foo(int data, int a, int b, int c, int d) :foo(data, a, b, c) {
        ma = data + a;
        ma += b;
        ma += c;
        ma += d;
        cout << "foo(int,int,int,int,int)" << endl;
    }
    void show() {
        cout << "ma = " << ma << endl;
        cout << "---------------------------" << endl;
    }
    int ma;
};
  • 当可以进行委托构造时的code: 
class foo {
public:
    foo(int data):ma(data){}
    foo(int data, int a) {
        ma = data + a;
        cout << "foo(int,int)" << endl;
    }
    foo(int data, int a, int b) :foo(data, a) {
        ma += b;
        cout << "foo(int,int,int)" << endl;
    }
    foo(int data, int a, int b, int c) :foo(data, a, b) {
        ma += c;
        cout << "foo(int,int,int,int)" << endl;
    }
    foo(int data, int a, int b, int c, int d) :foo(data, a, b, c) {
        ma += d;
        cout << "foo(int,int,int,int,int)" << endl;
    }
    void show() {
        cout << "ma = " << ma << endl;
        cout << "---------------------------" << endl;
    }
    int ma;
};

    整个过程是一个递归的过程,我们可以通过打印知道整个过程的执行顺序,需要注意的是:

  1)切不可构成环装的调用过程,比如将foo(int data, int a, int b) :foo(data, a) 改为foo(int data, int a, int b) foo(data, a, b, 1)类似这样的语句,否则程序会抛出异常‘;

  2)一旦初始化列表中进行了委托构造,即调用了其他构造函数,我们就不能再在初始化列表中初始化成员变量了;

4. C++ 14

    C++14引入了二进制文字常量、将类型推导从Lambda函数扩展到所有函数、变量模板以及数字分位符等。C++14 是对 C++11的重要补充和优化,是C++发展历程中的一个小型版本,虽然新增的内容较少,但是仍然为用户“带来了极大的方便”,为实现使C++“对新手更为友好”这一目标作出努力。

5. C++ 17

    到了2017年,C++迎来了C++17标准。此次对C++的改进和扩增,让C++变得更加容易接受和便于使用了。C++17引入了许多新的特性,比如类模板参数推导、UTF-8文字常量、fold表达式、新类型以及新的库函数等。

6. C++ 20

    C++20 的 Big Four(四大新特性:概念、范围、协程和模块)以及核心语言(包括一些新的运算符和指示符)。

6.1 模块 (Modules)

    module的引入至少有如下几个优点:

  • 更快的编译时间:
  • 宏的隔离:
  • 可以将大量代码划分为逻辑部分:
  • 让头文件成为“过去式”:
  • 摆脱丑陋的宏环境

    C++分离编译带来的一个问题就是编译会非常慢,因为C++ 的编译器在处理一个源代码文件的时候,首先要做的就是用相应的头文件的内容替换 #include 预编译指令。这就存在一个问题,对每一个源代码文件编译器都要重复一遍内容替换,这会占用大量的处理器时间。而引入module以后就不存在这个问题了,只需要import一下就可以在所有的源代码文件中使用,没有头文件的替换动作,使得编译时间可以大大减小。示例如下:

// helloworld.cpp
export module helloworld;  // module declaration
import <iostream>;         // import declaration
 
export void hello()        // export declaration
{      
    std::cout << "Hello world!\n";
}
// main.cpp
import helloworld;  // import declaration
 
int main() 
{
    hello();
}

6.2 协程 (Coroutines)

    协程,就是能够暂停执行然后在接下来的某个时间点恢复执行的函数,C++中的协程是无栈的(stack less)。使用协程可以方便的编写异步代码(和编写同步代码类似)。主要涉及三个关键字:

  • co_await:暂停当前协程的执行,直到等待的操作完成后继续执行。
  • co_yield:暂停执行并返回一个值,与return不同的是co_yield虽然返回了值 ,但当前函数没有终止。
  • co_return:用于结束当前协程的执行并返回一个值

  参考libo源码阅读的博客,可以更好的理解协程。

6.3 范围 (Ranges)

    range提供给我们一种函数式处理容器元素的方法,可以使得我们的代码简洁不少。以往此类代码的一般方法是在算法库函数上指定一个迭代器范围,搭配Lambda使用,但是在遇到transform这样的需求时我们不得不手动拷贝一个容器,在其之上做一些其他改动,range的引入使得我们可以非常简答的随意组合(用逻辑运算符就可以),并直接在容器上操作,其实可以看做一个简化版的MapReduce。

void cpp_11() 
{
    std::vector<int> v{1, 2, 3, 4, 5};

    std::vector<int> even;
    std::copy_if(v.begin(), v.end(), std::back_inserter(even),
                 [](int i) { return i % 2 == 0; });

    std::vector<int> results;
    std::transform(even.begin(), even.end(),
                   std::back_inserter(results),
                   [](int i) { return i * 2; });

    for (int n : results) std::cout << n << ' ';
    putchar('\n');
}

void cpp_20() 
{
    std::vector<int> v{1, 2, 3, 4, 5};
    for(int i : v   | ranges::views::filter([](int i) { return i % 2 == 0; })
                    | ranges::views::transform([](int i) { return i * 2; })){
        cout << i << " ";
    }
    putchar('\n');
}

6.4 概念与约束 (Constraints and concepts)

    概念和约束最大的作用就是可以在模板参数类型出现问题的时候不会一次报出几千行错误。我们可以使用concept来限制模板参数的类型。如果某处实例化的类型与concept相悖的话就会报错。当然含有concept的模板声明更像是一个特殊的实例化。下面的代码片段展示了一个简单概念 Integral 的定义和使用方式:

template<typename T>
concept bool Integral
{
    returnstd::is_integral<T>::value;
}

Integral auto gcd(Integral auto a, Integral auto b)
{
    if (b == 0) 
    {
        return a;
    }
    else 
    {
        return gcd(b, a % b);
    }
}

    Integral 这个概念需要 std::is_integral<T>::value 中的类型参数 T。std::is_integral<T>::value 这个函数来自 type-traits 库,它能在 T 为整数检查编译时间。如果 std::is_integral<T>::value 的值为 true,则没有问题。如果不为 true,则你会收到一个编译时间报错。gcd 算法是基于欧几里德算法确定最大公约数(greatest common divisor)。我使用了这个缩写函数模板句法来定义 gcd。gcd 要求其参数和返回类型支持概念 Integral。gcd 是一类对参数和返回值都有要求的函数模板。下面这段代码与上面等效:

template<typename T>
concept bool Integral
{
    returnstd::is_integral<T>::value;
}

template<typename T>
requires Integral<T>
T gcd(T a, T b)
{
    if (b == 0) 
    {
        return a;
    }
    else
    {
        return gcd(b, a % b);
    }
}


参考:http://c.biancheng.net/cplus/11/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++版本发展史 的相关文章

  • 年度回忆录(2011.12----2012.09)

    前几天刚刚参加了提高班十期的开学典礼 xff0c 最近师院的新生也陆 陆续 续的开始报道了 每年到这个时候都会感慨 年年岁岁花相似 xff0c 年年岁岁人不同 啊 对于提高班来说每年都有新的血液注入进来 xff0c 提高班的队伍在不断的扩大
  • python函数(变量,参数)

    函数的变量 1 xff0c 全局变量 定义在最外层的变量 xff0c 对于所有的内函数都能调用 2 xff0c 局部变量 定义在函数内的变量叫做局部变量 xff0c 在函数外是不能访问局部变量 注 xff1a 全局变量不能直接在函数内部进行
  • 程序员读书和练习的方法(个人观点)

    lt 传送门 gt 针对本文的交流探讨 gt 总宗旨 xff1a 打好计算机通用理论基础 通用实战能力 xff0c 便于需要时对各领域的无障碍深钻 时间宝贵 xff0c 不要为了学习而学习 计算机通用理论基础 xff1a 计算机各领域理论基
  • 从零开始的Ubuntu 16.04下PX4编译环境的搭建

    近来入手了一块pixhawk xff0c 想进行一些基于已有代码的二次加工 xff0c 于是到官网https dev px4 io 上看教程 官网上的教程是分为中文 英文以及韩文的版本 很多人肯定第一反应就是看中文的版本 但是这样做弊端真的
  • 驱动程序开发:SPI设备驱动

    目录 Linux下SPI驱动简介SPI架构概述SPI适配器 xff08 控制器 xff09 SPI设备驱动spi driver注册示例SPI 设备和驱动匹配过程编写imc20608六轴传感器SPI驱动设备树编写操作具体的imc20608驱动
  • 操作系统知识点(二)

    文章目录 内存管理程序执行过程内存保护 连续分配非连续分配基本分页存储管理方式基本分段存储管理方式段页式存储管理方式 虚拟内存局部性原理请求分页存储管理 内存管理 内存管理 Memory Management 是操作系统设计中最重要和最复杂
  • VR行业发展的前景和现状?

    标题 VR行业发展的前景和现状 xff1f 1 一个新事物的产生 xff0c 总是伴随着看好和唱衰两种声音 这两种态度自然有其可以理解的地方 xff0c 因为摆在我们面前的是未知 xff0c 而坐在餐桌前的两拨人 xff0c 站在不同的角度
  • 头文件与库的区别

    昨天突然问了一下什么是头文件 xff0c 我一听就傻了 xff0c 虽然上课的时候老师在讲编译的四个过程的时候说了一下 xff0c 但是还是不太理解 xff0c 我们知道编译过程中的预处理阶段会进行头文件展开 xff0c 宏替换以及条件编译
  • 进程、线程

    线程 xff08 thread xff09 线程其实是操作系统能够进行运算调度的最小单位 它是被包含在进程之中的 xff0c 是进程中的实际运作单位 一条线程指的是进程中一个单一顺序的控制流 xff0c 一个进程中可以并发多个线程 xff0
  • 基于Zynq7020双千兆以太网的数字信号处理板设计

    一 背景 背景 Xilinx公司在2010年发布了可扩展的处理器平台Zynq7000系列 xff0c 它采用了28nm工艺 xff0c 将FPGA与ARM cortex A9集成在一颗芯片上 xff0c 实现了高性能 高集成度 低功耗 Zy
  • 深入理解JS中的变量作用域

    在 JS 当中一个变量的作用域 xff08 scope xff09 是程序中定义这个变量的区域 变量分为两类 xff1a 全局 xff08 global xff09 的和局部的 其中全局变量的作用域是全局性的 xff0c 即在 JavaSc
  • 硬件工程师,从零开始无人机开发。

    毕业已经五年了 xff0c 一直在杭州某大厂 xff0c 做无人机硬件开发 无人机这块 xff0c 我进去的时候大厂刚开始 做 xff0c 有幸参与到整个无人机的硬件开发 我这个刚毕业的技术小白 xff0c 在这五年间成长了很多 无奈 今年
  • 个人总结:板球控制系统之串级PID整定方法,速度环与位置环,40S任务10S完成

    其实单环我们先出了所有题目 xff0c 但是效果显然没有串级PID的效果好 xff0c 有人需要的话可以把程序包发出来 xff0c 板球运行视屏也有 另外 xff1a 天下舵机参差不齐 xff08 哪怕型号相同 xff09 xff0c 想要
  • 树莓派3B+踩坑记录:一、安装Ubuntu Mate

    树莓派3B 43 踩坑记录 xff1a 一 安装Ubuntu Mate 树莓派 xff0c Ubuntu xff0c ROS硬件准备软件准备系统烧录安装Ubuntu Mate更换国内源网络配置开启ssh远程其他彩虹屏解决方案XShell和X
  • PointNet代码详解

    PointNet代码详解 最近在做点云深度学习的机器人抓取 xff0c 这篇博客主要是把近期学习PointNet的一些总结的知识点汇总一下 PointNet概述详见以下网址和博客 xff0c 这里也就不再赘述了 三维深度学习之pointne
  • 卡尔曼滤波通俗易懂的解释

    关于卡尔曼滤波 xff0c 网上的资料很多 xff0c 但是有很大一部分都是不断堆叠公式 xff0c 然后用各种晦涩难懂的专业术语进行解释 xff0c 说实话我刚开始看的时候也是云里雾里 xff0c 因此写下这篇博客是为了照顾和我一样的萌新
  • STM32通过PWM控制ESC30C电调

    最近在搞一个水下推进器 xff0c 这东西的控制其实跟四旋翼的螺旋桨控制差不多 但我也是第一次用STM32板子来控制电调驱动桨叶旋转 xff0c 因此踩了很多坑 网上找了很多资料 xff0c 但是很多都写的不是很清楚 xff0c 这边稍微记
  • STM32F7同一定时器多路输出PWM波通道之间相互影响问题

    2020 8 12更新 这次用Cube直接生成PWM控制代码 xff0c 然后再RT Thread Studio上编写程序 xff0c 发现可实现TIM1和TIM8的8路PWM波调控 xff0c 因此上面论述的问题可能是自己在写底层时有某些
  • Ardusub源码解析学习(一)——Ardusub主程序

    APM Sub源码解析学习 xff08 一 xff09 Ardusub主程序 前言一 准备工作二 Ardusub cpp解析2 1 scheduler table2 2 schedulerget scheduler tasks setup
  • Ardusub源码解析学习(二)——电机库

    Ardusub源码解析学习 xff08 二 xff09 电机库学习 一 RC输入与输出1 1 RC Input1 2 RC Output 二 电机库学习2 1 setup motors 2 2 add motor raw 6dof 2 3

随机推荐

  • Ardusub源码解析学习(三)——车辆类型

    APM Sub源码解析学习 xff08 三 xff09 车辆类型 一 前言二 class AP HAL HAL三 class AP Vehicle3 1 h3 2 cpp 四 class Sub4 1 h4 2 cpp 五 总结 一 前言
  • 年度回忆录(2012.10----2013.01)

    寒假结束了 xff0c 年也过完了 xff0c 提前回来一天就开始着手补上这迟到的年终总结 xff0c 写了一个多星期还觉得有些东西没有写出来 xff0c 无奈 xff0c 点到为止吧 2012 年的后半年经历了很多 xff0c 收获了很多
  • Ardusub学习——飞行模式

    参考资料 xff1a Ardusb官方手册 Sub Rework joystick input and pilot input in general Flight Modes Ardusub支持多种飞行模式 xff0c 但是其中一部分需要有
  • Ardusub源码解析学习(五)——从manual model开始

    Ardusub源码解析学习 xff08 五 xff09 从manual model开始 manual init manual run 从本篇开始 xff0c 将会陆续对Ardusub中各种模式进行介绍 xff0c stabilize mod
  • 重读Ardupilot中stabilize model+MAVLINK解包过程

    APM源码和MAVLINK解析学习 重读stabilize stabilize modelinit run handle attitude MAVLINK消息包姿态信息传输过程 之前写的模式都是基于master版本的 xff0c 这次重读s
  • QGC添加自定义组件和发送自定义MAVLINK消息

    QGC添加自定义组件和发送自定义MAVLINK消息 一 添加自定义组件1 1 在飞行界面添加组件1 2 实现组件事件1 3 在MOCK模拟链接中实现验证1 4 验证 二 自定义MAVLINK消息的一些预备知识三 QGC自定义MAVLINK消
  • MAVLINK消息在Ardupilot中的接收和发送过程

    MAVLINK消息在Ardupilot中的接收和发送过程 SCHED TASKupdate receive update send 由于现在网上很多的都是APM旧版本的解释 xff0c 因此把自己的一些学习所得记录下来 截至写博客日期 xf
  • Ardupilot姿态控制器 PID控制流程

    Ardupilot姿态控制器 PID控制流程 一 PID姿态控制器1 1 Copter姿态控制官方原图1 2 ArduCopter V4 X STABILIZE 二 姿态控制器类实现2 1 类成员解析2 1 1 类成员变量2 1 2 类成员
  • APM姿态旋转理论基础

    APM姿态旋转理论基础 一 坐标系1 1 NED坐标系1 2 机体坐标系 二 欧拉角姿态变化率与机体角速度的关系 三 旋转矩阵3 1 基本公式3 2 矩阵作差3 3 旋转矩阵与变换矩阵的区别 四 DCM五 轴角法5 1 基本概念5 2 与旋
  • 详解APM的开方控制器sqrt_controller

    前言 前面说过 xff0c sqrt controller是对P项进行整定用途的 xff0c 目的就是让P项的控制响应 软 下来 xff0c 实际上就是一个经过改进的P控制器 读懂了sqrt controller xff0c 那么你对APM
  • Ardupilot前馈及平滑函数input_euler_angle_roll_pitch_yaw解析

    Ardupilot前馈及平滑函数input euler angle roll pitch yaw解析 源码解析这个函数做了什么部分细节euler accel limit input shaping angle 姿态变化率与机体角速度之间的关
  • Ardupilot倾转分离函数thrust_heading_rotation_angles

    Ardupilot倾转分离函数thrust heading rotation angles 什么是轴角分离源码分析一些细节补充效果显示及进一步修改 本文主要写一下自己对于APM倾转分离 xff08 轴角分离 xff09 函数的一些学习笔记及
  • Spring IOC原理解析

    首先恭喜守宏同学找到了自己心仪的工作 xff0c 入职的事情终于尘埃落定 xff0c 也算是一个新的开始吧 和守宏聊天的时候也说了很多有关工作的事情 xff0c 畅想了以后美好的未来 xff0c 也想到了今后的种种困难 不说别的就是单单在北
  • Ardupilot四元数姿态控制函数attitude_controller_run_quat解析

    Ardupilot四元数姿态控制函数attitude controller run quat解析 源码解析细节讲解thrust heading rotation angles update ang vel target from att e
  • Ardupilot速率控制器rate_controller_run解析

    Ardupilot速率控制器rate controller run解析 PID速率控制器源码解析rate controller run PID运算积分限制update i get ff set xxx 内容补充 xff1a 函数中陀螺仪数据
  • muduo网络库学习总结:基本架构及流程分析

    muduo网络库学习 xff1a 基本架构及流程分析 基本架构Basic ReactorMutiple Reactor 43 ThreadPool muduo库的基本使用基本结构介绍EventLoop类Poller类Channel类TcpC
  • push_back和emplace_back比较以及vector扩容

    push back和emplace back比较以及vector扩容 push back和emplace back的比较使用测试类测试过程将实体类对象传入将右值数字传入将实体类对象move 转右值之后传入 vector扩容过程 关于这部分内
  • 在ubuntu 11.04下编写驱动程序

    在ubuntu11 04下直接就可以编写驱动程序 xff0c 并进行编译 hello c include 34 linux init h 34 include 34 linux module h 34 static int hello in
  • ROS的优势与不足(除了ROS 机器人自主定位导航还能怎么做?)

    导读 xff1a 随着这两年国内机器人的升温 xff0c 自主定位导航技术作为机器人智能化的第一步正不断引起行业内的重视 为了实现这一功能 xff0c 不少厂家选择采用机器人操作系统ROS xff08 Robot Operation Sys
  • C++版本发展史

    1 C 43 43 98 2 C 43 43 03 3 C 43 43 11 3 1 nullptr 3 2 auto 3 3 decltype 3 4 初始化列表 3 5 范围for循环 3 6 右值引用 3 7 字符串字面量 3 8 n