不完全解读C++之父谈C++20标准

4,200 阅读15分钟

封面13 拷贝.png

作者:老九—技术大黍

B站视频:C++ 20标准--C++走的40载

社交:知乎

公众号:老九学堂(新人有惊喜)

特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系笔者授权

前言

在油管上有这样一出视频:CppCon 2019: Bjarne Stroustrup “C++20: C++ at 40”

这是2019年9年17号,C++之父在2019年C++大会上发言,他以1979年开始使用“C with Classes”(即C++的前身)起始点,到2019年为一个截止点,Bjarne Stroustup总结了C++走过的40年历程,最终产出一个结果:C++ 20标准稳定版本呈现给广大的C++开发者。

C++历史简介

image-20210330164351987.png

我们以维基为标准来参见说明C++的发展历史,以上内容我们就不再参考了,请大家自行百度翻译理解。我们今天着重不是讲C++的历史,我们今天是针对C++之父Bjarne Stroustup的对C++发展40年的视频的解读。

下面图示配上我们讲解,希望能够帮助大家快速理解C++之父对C++语言和它的发展历程的描述,从而早日能够学习会C++这种最牛批、最装逼的编程语言。我们录的视频已经上传到 B站了,以方便大家观看相应的视频。

作为计科系程序员的我,下面所有的参考翻译和解读,请大家都不要当成标准来理解。各们看官就当看一篇游戏解说或者戏说之类的文章(因为我是C++的拥戴者,忠粉啊image-20210330171322240.png~,所有难免解读之中会带主观情绪和观点),请大家阅读时本文时要抱着轻松的、愉快的心情来读就好了。如有不足之处,请大家指正和补充。

C++ 20版本的特点

image-20210330164956726.png C++之父用两个单词总结了C++ 20版本的特性

  • stability--稳定(稳定压到一切)
  • evolution--更新(与时俱进,走到潮流之巅)

注意:对比Bjarne Stroustrup年轻时在贝尔试验室当和现在的照片,我发现老Bjarne现在已经快要成仙的节奏啊,我觉得他非常的帅!

他现在像不像

image-20210330171900088.png

铁拳》游戏中的英雄Heiharchi啊image-20210330171322240.png

反正我是怎么看,他现在都像这个大Boss的存在啊~ 点个赞!image-20210330172114418.png

C++发展历程

1977到2019

image-20210330165505396.png

从1979年起,C++就已经被用在最初的座机电话的开发当中,并且一直到现我们智能手机都一直使用C++来进行开发,C++在硬件底层方面的应用开发从来都没有间断过。

上面这张图示描述了,在过去电话产品使用了硬件接口的产品和现在无线接口的智能手机产品,它们的内部都是使用C++书写的软件驱动程序来被第三应用程序调用的。

1980年代早期

image-20210330170515863.png

C语言之父Ken和Dennis只是提供一款不依赖汇编就可以完成几乎所有功能的产品,它一款半移植性的系统编程语言--C语言。说它是半移植性原因有两点:

  • C语言没有函数原型
  • Lint只是表示静态程序分析器的状态

:Lint的概念如下所示

image-20210331184517773.png

lint简单地讲,它就是一种编程语言词法和语法分析器。

大多数的计算机应用程序可用内存都是小于1MB的,并且运行频率也小于1MHz的状态,因此在这个时代高科技东西如下:

  • 拥有PDP-11 16位小型计算机是非常有逼格的
  • 拥有一台VT100终端,那就是拥有一种艺术品
  • 一台“个人电脑”售价大约3000美刀(那可是通涨了几十年前的3000美刀啊)
  • IBM PC此时只一台概念机

这个时代“所有人”都“认为”现在逢人必说的“面向对象思想”是无用的,原因很简单:

  • OO的东西运行太慢
  • 使用场太少,要求非常特殊
  • 对大多数来说,它实在太难以理解了

进入到2019年代

image-20210331185742083.png

这里我们讨论C++20版本特点:

  • 新不等于好不等于旧,并且新不等于坏不等于旧
  • 本文针对不是单独针对C++ 98、C++ 14、C++ 20版本等
  • 我会偶尔简介一下C++的历史

本文不讨论“细节”,原因是:每个技术点在本周之内会一小时来讲解。

建议的学习方法论

开发人员使用C++的态度

  • 聚焦C++的本质(就是通常所说,看问题不能只看问题表面,要看问题的本质 )
  • 只有在需要时才使用C++的“高级特性”

C++教学人员的态度是

  • 聚焦C++的本质
  • 不要因为篇幅所限就隐藏掉C++的关键特性
  • 告诉人们三个真相
    • 真相只有一个
    • 但是真相不总是会出现
    • 不断增加细节信息

区别什么是合法和有效

需要有比较好的工具支撑,比如C++核心向导

image-20210331204211491.png

C++的原则性和取舍性

  • C++是一款可以进行业务定义、功能实现和可以使用轻量级抽象的编程语言
  • 编程语言设计不仅仅是一个用来开发的产品,它还应该包含如下两点
    • 延续性的逻辑才是本质
    • 稳定压到一切
  • C++能为代码做的事情:
    • 世界是我们难以想象的复杂
    • 世界也是混沌的
    • 大部分的应用要求稳定压到一切
    • 通常来说,C++是“只做一件事情”
  • 支撑“随意使用”的特性
    • 不强迫开发人员必须遵守某些“纯理论性的东西”(比如面向对象编程思想)
    • 不会要求只能是“专家”级别的开发人员才能使用
    • 简单化编程思想原则(复杂的事情简单化,简单的事情不能复杂化)

C++的高阶目标

image-20210331211123803.png

C++的高阶目标是遵守aka原则:

  • 演化
    • 稳定(向下兼容,C++ 20版本编译器一样可正常编译运行C++ 98版本的源程序)
    • 支持代码升级
  • 简化是一切核心
    • 不要把复杂事情复杂化
    • 不要把复杂事情变得任性难懂
  • 零成本原则
    • 不使用的,我们不用关注(也是aka的“不用发散原则”)
    • 我们需要的功能,不是需要什么都要自己手写
  • 高阶目标
    • 核心功能是由我们自己设计并且软件实现
    • 我们开发人员想法就是业务功能本身

补充:aka principles

image-20210331214239243.png

这里我就不再参考翻译了,请大家自行百度参考理解了。因为这种吹牛皮的文章是很多的,我们就不在这里累述了。

我们改变世界

image-20210331212956004.png

对于应用程序来说,编程语言的价值就是实现应用的品质同!

image-20210331213210316.png

软件改变世界,体现如下两个方面:

  • 编程和设计
    • 抽象:直接表述自己的想法
    • 使用更好的硬件
    • 代码分析和编译构建操作
  • 应用领域
    • 伸缩性和复杂性的应用领域
    • 工程师、科学家等领域
    • ...

image-20210331213943814.png

从1979年到2019年,C++已经在非常的领域被广泛使用了。

C++的关键特性

image-20210401170721384.png

  • 静态类型系统同时支持内置类型和用户自定义类型
  • 支持值和引用语义
  • 直接使用机器资源和操作系统资源
  • 系统和通用资源管理
  • 支持混合软件开发模式
  • 支持面向对象编程
  • 支持编译时编程
  • 通过库支持内置并发 补充

image-20210401171849197.png

我这里不参考翻译,大家自行百度理解。网上吹牛批的文章很多,我只是简单描述一下:编译时的概念是指应用程序代码被转换成机器码的时间(比如二进制代码),并且通常它是出现在运行时之前。

静态系统

image-20210401172452958.png

静态类型系统是所有C++功能的基础,它支持如下核心功能:

  • 编译时错误侦测
    • 比如,list lst; ... sort(list);//报错:不能随机访问链表
    • 运行时处理错误时会导致运行成本高,并且复杂度更高
  • 执行效率
    • 直接表述了我们的想法,简单高效
    • 把内部计算从运行时前移到编译时
  • 通过编译机制的解决方案来实现灵活性
    • 解决计算过载问题,比如 sqrt(2)
    • 泛型编程,比如 vector v; ... auto p = find(v,42);
    • 支持meta(位元)编程(这个在现代编程语言中是非常流行的)
    • 编译时诊断,比如静态static_assert(weekday(August/3/2019) == Sunday)的静态断言

注: 正是因为C++使用的编译时进行类型侦测、过载处理、静态断言等处理,这样它花费的编译时间一定很长,所以我们原来使用C++ 11编写游戏时,因为分模块设计,直接使用Java和C#的编码思想来进行C++编程(所有功能都使用一个C++项目来编写、编译、调试),结果在程序代码上3万行之后,那个编译速度简单慢得像老狗!

值和引用语义区别

image-20210402105752260.png

们需要两个概念来描述事物的体质。

值的语义可以给任何数据类型赋值使用,如下所示:

x = y + z; //变量类型的可以int内置类型,或者complex<double>,或者是矩阵,亦或者是其它...
x = y; //x是y的副本;但是x和y分别都是独立的东西。就像玄幻小说中的本尊和分身的概念。

指针/引用语义可以赋值给任何类型,如下所示:

*p = x; //x的值可以是T*, 以及shared_ptr<T>,亦或是其它...
p = q; //p和q指向同一个东西。
  • handle--句柄(操作系统分配的内存地址)
  • value--值(在内存中被保存的东西)

image-20210402110657510.png

值类型

  • 大部分类型都是值,比如整数、字符、字符串、容器类型等
  • 抽象语义(通常指正则表达式)
  • 通过栈分配内存空间
  • 内联扩展
  • 一般使用指针实现

注:inlining的概念参见下面所示

image-20210402112333947.png

我在这里就不参考翻译了,关于内联概念,网上吹牛批的一大堆,请大家自行百度参考。

指针/引用类型

  • 所有指针类型和引用类型都可以“指向”一个东西,比如T*, T&, unqiue_ptr,Forward_Iterator
  • 使用指针/引用的核心目标之一是高效传输信息数据,比如
    • auto p = find(lst,"something interesting");
    • sort(v);
  • 使用指针/引用的核心目标之二是构建非分散的东西(数据结构)
  • 指针/引用都使用机器资源,因而可以达到效率最优的效果

关于泛型

image-20210402113722947.png

泛型编程的本质是规律性,规律性的即针对C++的内置数据类型,同时也针对用户自定义类型。参见下面的示例代码:

template<Element T> class Vector{
public:
    Vector(initializer_list<T>);
    //...
    T* elem; //T*指向C++中任意合法的数据类型,包括自定义数据类型
}
//在使用时参数化Element的类型如下:
Vector<int> vi = {1,2,3}; //C++内置数据类型
Vector<complex<double>> vc = {{1,2},{3,4},{5,6}}; //用户自定义数据类型
Vector<Vector<int>> vvi = {{1,2,3},{4,5,6},{7,8,9}}; //嵌套类型

直接使用机器资源

image-20210402114409631.png

  • 主要操作是影射机器指令集:
    • 算法运算:+,-,*,/,%
    • 访问运算:->,[],(),...
    • 位逻辑运算:&,|,^(异或),~(补码),>>和<<(位移),旋转
  • 内存是一组序列化的东西,比如指针是机器地址如上图所示
  • 对象可以被简单的串联组合而成,如上图所示
    • 数组
    • 类/结构体
    • 注意:handle也有一个自己的值
  • 对硬件进行简单的抽象

image-20210402115133682.png

位集

  • 操作任意尺寸的位
  • &,|,^&,|,^(异或),~(补码),>>和<<(位移),旋转

跨度

  • 操作对象序列,比如

    • 数组array<byte, 1024> a;

      //...

      span s{a}; //没有显示声明元素类型的尺寸大小

      for(const auto x : s) f(x); //没有范围错误检测

      for(auto& x: s) x = 99;

      span s2{a, 512}; //指定一个尺寸的跨度

      span s3{a}; //指定元素类型的跨度

洋葱法则

image-20210402133652564.png

  • 抽象层
    • 我们抽象结果就像洋葱一样,剥离抽象得越,那么我们哭得会厉害
  • 平均复杂度管理

大神的意思是:凡是都有两面性。如果我们玩面向对象封装的概念越深入,那么必然复杂度会增加。如果不玩面向对象编程思想,那么代码越多,那么同样的复杂度也会越来越高。

构造/析构对

它对象资源管理的架构:参见下面的Gadget类

  • 所有对象资源在我们使用之后必须归还给操作系统
  • 用户不需要知道哪个工具资源被使用
class Gadget{
    Gadget(/*参数*/); //初始化/构造对象
                     //从操作系统获取内存资源
    ~Gadget(); 	     //清除资源
    //...复制和移动...
    //...重置用户接口...
private:
    //...呈现...
}

系统通用资源管理

image-20210402134758349.png

  • 每个对象都一个独立的资源,它表现为

    • 需要自我清理(析构函数)
    • 不能使用内置类型指针来管理这种关系
  • 标定资源范围,参见下面的示例代码

    void f(int n, int x)
    {
        Gadget g{n}; //我们第三方开发人员不需要知道内存资源具体被操作系统怎么管理
        //...
        if(x < 100) throw run_time_erro{"Weird!"}; //不会产内存泄漏
        if(x < 200) return;   //不会产内存泄漏
        //...
    }
    

注意:在C++编程中,我们经常会看到return的写法,这种写法是为了不产内存泄漏。

image-20210402135421580.png

  • 控制对象复杂的生命周期有四种方式:创建、复制、移动和析构。参见下面的示例
Gadget f(int n, int x)
{
    Gadget g{n}; //对象g可能会很大
                 //对象g可能包含不能复制的对象
    //...
    return g; //不会产生内存泄漏,因为没有复杂
    			//没有指针
    			//没有显示声明内存管理
}
auto gg = f(1,2); //把Gadget对象移动到f函数之外,保存另外一个变量gg中

从图示看到,对象g和gg是两个对象,但是它们的内容是相同的,并且内存存贮的位置是一样的。

通用资源管理

image-20210402140201557.png

  • 实现隐含方式的内存资源释放与安全保障
  • 所有C++标准库容器都会管理它管理它们的元素,这些容器具体包含如下
    • vector
    • list, forward_list(单向链表)
    • map, unordered_map(哈希表)
    • set, multiset
    • string, path
  • 很多C++标准库类会管理非内存资源,具体包含如下
    • thread, jthred, shared_mutex, scoped_lock
    • istream, fstream
    • unique_ptr, shared_ptr
  • 容器可以包含非内存资源,比如递归操作,vector<forward_list<string,jthread>>

模块

image-20210402140802780.png

模块是现代编程语言最热门的概念,在JavaScript中到处使用,包括在Qt框架语言更是成功的证明分模块设计和开发是多么的强大。下面是标准C++的模块语法使用示例:

export module map_printer; //定义一个模块

import std;   				//导入模块,导入的顺序不重要
import my_containers;

export 
template<forward_range S>
    requires Printable<KeyType<S>>&& Printable<Value_type<S>>
void print_map(const S& m){
    for(const auto& [key,val]: m) //切分成键和值
        cout << key << "->" << val << '\n';
}

image-20210402141226890.png

  • 在C++的模块有如下主要作用:

    • 最小化对象之间的依赖性

    • 避免循环依赖

    • 实现模块化编程

      import A;

      import B;

      它与下面的作用是一样

      import B;

      import A

    • 只使用导入模块中代码,从而可能调整代码的大小

    • 只对模块中的代码”复制”一次,并且也只是解析一次

组装

image-20210402141621428.png

C++支持以下几种组装方式:

  • 模块
  • 概念
  • 模板
  • 函数
  • 别名

泛型编程

根据以上可以使用的组装策略,我们再来看一下现在C++20版本的泛型编程

image-20210402141755057.png

  • 直接使用类型来影射抽象的业务逻辑需求,比如直接映射单向链表,整数类型等
  • 把业务逻辑需求定义成一概念类型,参见下面的示例代码
template<typename R>
concept Sortable_range =
	random_access_range<R>				//有begin()/end(),++,[],...
	&& permutable<iterator_t<R>>		//有swap()等
	&& indirect_strict_weak_order<R>;	//有<等

使用方式

void sort(Sortable_range auto&);
sort(vec);	//OK:保存为一个有顺序数列 
sort(lst);	//错误:试图把一个链表保存为有序列的样式 

image-20210402144641678.png

  • 根据抽象需求来选择

    void sort(Sortable_rang auto& container);	//容器必须是可排序的
    
    template<typename R>
    concept Forward_sortable_range =
    	forward_range<R>
    	&& sortable<iterator_t<R>>
    
    void sort(Forward_sortable_range auto& seq); 	//不需要随机访问
    
    sort(vec);	//OK:因为使用Sortable_range排序
    sort(lst);	//Ok:因为使用Forward_sortable_range
    
  • 我们不能说

    • “Forward_sortable_range没有Sortable_rangeg严格”
    • 我们根据定义来计算

image-20210402145226378.png

  • 泛型编程(GP)“就是一种编程方式”,具体包含如下
    • 使用概念定义一个接口
    • 类型指定和接口外加一层
    • 从原则上讲,sort(v)和sqrt(x)是有一点不同的
    • “类似于普通的编程方式,但是完全一样”
  • sort函数默认使用<比较方式来排序,当然我们也可以指定不一样的排序方式,参见下面的示例代码
template<random_access_range R, class Cmp = less>
    	require sortable_range<R,Cmp>
constexpr void sort(R&& r, Cmp cmp = {});

sort(v,[](const auto& x, const auto& y){return x > y;});
sort(vs,[](const auto& x, const auto& y){return lower_case_less(x,y);});

面向对象编程

image-20210402145912811.png

这张PPT我就不参考翻译了,大家都看得懂。我们只说一个重点:面向对象的思想目前只能在运行时实现。

image-20210402151103702.png

这张PPT的意思是说,如果我们不在运行时来实现面向对象编程思想,那么从目前来看只有通过静态存贮方案来解决。

image-20210402151240610.png

在C++20版本中不会及时加载overloaded()函数了。现在的这个版本的C++是可扩展的

  • 根据我们第三方开发人员的需求来构建
  • 或者使用现有标准来构造
template<class... Ts>
struct overloaded: Ts...{		//收集N种类型
    using Ts::operator()...; 	//N种类型都呼叫一次
};

//推导出来模板参数类型
template<class... Ts> overloaded(Ts...)->overloaded<Ts...>

直接使用操作系统资源

image-20210402151831418.png

  • 简化了内存锁
mutex m1;
int sh1; 	//共享数据

mutex m2;
int sh2;	//另外一个共享数据

void obvious()
{
    //...
    scoped_lock lck1{m1,m2}; 	//获取两把锁
    //操作共享数据
    sh1 += sh2;
}//释放两把锁

image-20210402152528804.png

  • “双倍锁定初始化”操作
mutex mx;		//OS支持同步操作,代码成本很高
automic<bool> initx;	//使用相对方便的atomic变量
int x; 			//共享数据
if(!initx){
    lock_guard lck {mx};
    if(!initx) x = 42;
    initx = true;
}
//...使用x变量...
  • 无数据竞争

image-20210402152924094.png

同步的代码总低级别方式书写

mutex mx;
automic<bool> initx;
int x;
if(!initx.load(memory_order_acquire)){
    mx.lock();
    if(!initx.load(memory_order_relaxed)){
        x = 42;
        initx.store(true, memory_order_release);
    }
    mx.unlock();
}
//...使用x变量...
  • 原则上不要对这些低级别代码进行进行抽象

image-20210402155514276.png

我看一下C++20版本之前的线程同步代码实现

// example for thread::join
#include <iostream>       // std::cout
#include <thread>         // std::thread, std::this_thread::sleep_for
#include <chrono>         // std::chrono::seconds
 
void pause_thread(int n) 
{
  std::this_thread::sleep_for (std::chrono::seconds(n));
  std::cout << "pause of " << n << " seconds ended\n";
}
 
int main() 
{
  std::cout << "Spawning 3 threads...\n";
  std::thread t1 (pause_thread,1);
  std::thread t2 (pause_thread,2);
  std::thread t3 (pause_thread,3);
  std::cout << "Done spawning threads. Now waiting for them to join:\n";
  t1.join();
  t2.join();
  t3.join();
  std::cout << "All threads joined!\n";

  return 0;
}

原来的同步做对要写20多行,现在只用两三行了。

image-20210402155651963.png

并发算法

image-20210402160427964.png

没有必要时不要对线程进行同步操作,让它们尽情的自由发挥吧。

image-20210402160648071.png

C++20版本可能不能及时提供并发版本的排序功能。

编译时计算

image-20210402160753106.png

C++20版本把过去运行时计算移植到编译时计算了,理由如下:

  • 基于执行效率和代码的优雅度
  • 只做一次,而不是像过去重复百万次的计算
  • 再也不需要运行时的错误句柄了
  • 常量不再会存在数据竞争问题

编译时可以用解决以下问题:

  • 重载和虚函数
  • 模板
  • 可变模板
  • 常量表达式函数和用户自定义类型

image-20210402161405897.png

在编译时调用常量参数。

image-20210402161527780.png

注意下面的语句写法

cout << weekday{June/21/2016} << '\n';
static_assert(weekday{June/21/2016} == Tuesday);

这种写法就是JavaScript中最流行的JSON语法嘛。

直接使用硬件

image-20210402162257298.png

  • 函数--直接使用内存栈框架
  • 协程(Coroutines)--使用内存调用框架

协程

image-20210402162544236.png

协程是一种的生成器和管道,它主要赖加载。

int main()
{
    auto src = seq(2);			//无限int序列:2,3,4,5,6,7,8,9,10,11,...
    auto s = sieve(src);		//过滤非素数:2,3,5,7,11,...
    auto t = take(s, 10'000);	//获取前10000个素数:2,3,5,... 104729
    print(t);					//打印素数
}

image-20210402163304603.png

这张PPT我就不再参考翻译了,大家一看就懂了。

image-20210402163414771.png

这个是协程计算输出的结果。

image-20210402165911630.png

using更方便我们的对类型进行重定义!

image-20210402170032290.png

协程的核心目标是简单异步编程。

image-20210402170716246.png

C++20版本可以轻松使用其它任何编程语言书写写的库,当C++可以使用除自身标准库之外的其它编程语言的库。

image-20210402170951575.png

标准库我就不参考翻译了,像其它库,比如Boost,Qt框架编程库,这个库太好用了啊。

时间处理

image-20210402171131197.png

image-20210402171155612.png

这两张PPT我不参考翻译,因为一看就懂了。

什么是C++20?

image-20210402171352297.png

C++20版本是继C++11版本之后最重要的发布版本 ,它有如下特性:

  1. 简单、优雅,更快的编码速度和更快的编译速度
  2. 模块化编程
  3. 概念定义
  4. 协程
  5. 范围指定
  6. 日期
  7. 范围跨度
  8. 更优秀的编译时编程支持
  9. 非常多的小功能,但是这些功能确非常重要

image-20210402171819003.png

  • C++是一款定义和实现轻量级抽象的通用编程语言。
  • 不是一个复杂的功能包
    • 它是一组想法
    • 它是一组设计原则
  • 它经历这样一个演化过程...->C++98->C++11->C++14->C++17->C++20->...
  • 它是一个标准WG21(C++标准协议)

image-20210402172155899.png

C++20版本以后的特性

image-20210402172238112.png

在C++23版本中会额外功能:

  • 标准模块
  • 支持协程的库
  • 执行器和网络编程

可能会有的功能

  • 静态反射
  • 设计模式匹配

**说明:**可能大家对静态反射和设计模式匹配不敏感意味着什么。如果C++实现了反射功能,并且C++语言自身能够匹配设计模式,那么意味着现在最吃香的Java语言该退出历史舞台了。

总结

image-20210402172656151.png

C++20很牛批!

虽然,现在是2021年,我们VS 2020版本还没有发布出来。但是,我们还是非常期待C++20版本的正式在实际开发中的应用。

最后

记得给大黍❤️关注+点赞+收藏+评论+转发❤️

作者:老九学堂—技术大黍

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。