C++新标准特性初探

说实话,这一套东西东拼西凑到处取经,不过有些还挺香的.

目前对于14,17的特性,GCC和Clang都完成了很好的适配.在C++20上,下面提到的特性都在最新的版本里完成了绝大多数.

标准定得爽,到了实现就凉凉.

auto

这个关键字…是真的…相信不久之后c++只需要下面一个参数.

1
2
3
auto auto(auto...){
return auto;
}

这个东西,就是把变量类型交给编译器来考虑.这个特性实际上早该有了,因为c++编译器原本就会检查右值和左值的类型是否匹配,那么顺便填一个左值的类型也不会累着它.

这个也挺常用的.for(auto it=vec.begin();...这种写法省了很大工夫.

和它配合很好的是新引入的语法糖for(auto item:var),写起来很爽.这玩意也支持引用.另外一个值得注意的是在C++17后,由模式绑定加成,这玩意能进一步写成for(const auto& [key,value] : map).

还有一个东西是auto用在函数签名的返回值时需要尾随返回值类型.

1
2
auto f() -> ???{
}

在C++14里不需要再尾随返回值类型了.不过另外还有一种写法decltype(auto),这两个的区别在于后者能够保留引用的类型信息.这个尾随返回值的用处就剩下在lambda表达式里用了.不过,尝试在签名里原本的返回类型位置写auto,转而把真实的返回类型尾随,这种写法越发像现代语言了.

1
2
fn f() -> i32 {
}

Lambda 表达式

好用的东西.

1
2
3
sort(vec.begin(),vec.end(),[](const Student &a,const Student &b){
return a.age<b.age;
})

再也不需要cmp_1,cmp_2了.

lambda表达式作为一个闭包,需要考虑其对于环境变量的捕捉类型.

  • 按值捕获
  • 按引用捕获

就如它们字面上的意思.实际实现时,将其写在[]内.例如[=],[&],[foo,&bar].一个要注意的问题是,按值捕获时,捕获的变量均为const,不能修改,后缀[]()mutable{}后可以修改,不过这种修改不会影响环境变量.

在C++14中可以借助auto实现lambda多态.

1
auto func=[](auto a){return a+a;};

然后这个func可以直接传很多参数.

另一个是在捕获时进行环境变量初始化,例如[x=0].不过这玩意是否有额外的考虑…大概能去看看RFC?

在C++20中,lambda可以携带模板[]<typename T>(){}.

using

以前,我们定义ll有这么两派.

1
2
#define ll long long
typedef long long ll;

现在,新的一派加入了战场.

1
using ll=long long;

这几种办法如果非要分出个高低,那大概要从下面几个方面说

  • define是一种替换.它相对于另外两种来说,有些过于强大和随性,可能会带来问题.
  • typedef是C风格的.
  • using是C++风格的.另外using在内部的实现是基于模板的.这一点是否会带来好处我还没弄明白.

nullptr

赋值给空指针的新值,用来取代NULL.这个关键字对于工程的用处更高.它用于处理和重载,泛型编程有关的问题.

一个空指针在C风格中定义为宏NULL,这个东西,它是0的别名.那么,一个泛型函数到底该把它看作int还是指针?这也是不推荐用define定义新类型的一个理由.说不定什么时候就踩进坑.

decltype

对类型的运算,用于在编译时获取某个值的类型.算是个和auto配套的东西.因为auto了,你也不知道它到底是个什么,此时就可以

1
2
auto a=/*something weired*/;
decltype(a) b=a;

怎么说呢…有了一种动态语言的调调.


用以上这些东西,就能写出这种代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//当然这段代码是没啥意义的,只是用一下新的特性
template <typename T>
auto func(string type)->function<T(T,T)>{
if(type=="add"){
return [](auto x,auto y){
return x+y;
};
}else{
return [](auto x,auto y){
return x-y;
};
}
}

//可变参数
template <typename... T>
decltype(auto) reduce(auto f,const T... args){
const auto vec={args...};
auto res=*vec.begin();
for(auto ne=vec.begin()+1;ne!=vec.end();ne++){
res=f(res,*ne);
}
return res;
}

int main() {
cout<<reduce(func<int>("add"),1,2,3)<<endl;
return 0;
}

constexpr

好了,现在对于

1
2
const int MAXN=10;
int a[MAXN];

我们有了新的写法.

1
2
constexpr int MAXN=10;
int a[MAXN];

实际上,下面的写法才是对的.上面的写法按照标准应该是无法通过编译的,只不过它太常用,编译器知道你想干嘛,所以默默完成了操作,没有人抱怨,可喜可贺.

constexpr用于在编译阶段进行求值.也可以用在函数.以往有些人在模板上玩花,在编译阶段就让编译器率先完成类似于打表之类的操作,以至于评测器陆续全部完善了编译时限制时间.现在再想玩这些操作,可以直接使用constexpr了.不过可惜的是它还C11里比较菜,不能完成复杂运算.直到在C14里,constexpr能够支持if甚至递归了.

在C++14里,constexpr能够和template配合实现对不同类型常量的初始化.这一点基本上就把#define PI 3.1415926535897932385打爆.推荐使用#define定义PI的原因就在于它的替换行为能够免去在常量定义过程中被类型精度影响.

右值引用

这玩意的存在意义主要是消除不必要拷贝,并给泛型编程带来便利。

一般来讲,当我们执行如下的代码

1
2
Student student;
student = Student(&#039;Tiansuo Li&#039;,24);

第二行的赋值过程中发生了内存的复制和销毁。对……这个学生的信息并没有直接给student。

据说现代编译器能够针对这种情况进行优化了。

右值有这么一个特性,只能被使用一次。这是非常重要的一个特性,例如字符串、常量,这些东西除了在作为右值赋值给左值的一瞬间,其他时候再无用处。那么在这种情况下,仍然对此类右值执行复制就显得很累赘。右值引用就是用于解决此类问题的。

右值引用使用&&标注。

为Student实现以下右值引用语义

1
2
3
4
5
6
7
8
9
10
Student& operator=(Student &&stu){
if(this !=&str){
name=stu.name;
age=stu.age;

stu.name="";
stu.age=0;
}
return *this;
}

当然复制一个名字什么的还没有这么大区别。

std::move这个关键字和上面的右值引用有点关系。它指明对象的赋值带有资源的转移,也就是强制进行右值引用和资源移动(比如上例中学生名字的移动)。

这种感觉是不是和rust有点相似?不过很可惜这不是一种东西。至少它在最初设计上不是。但是当和一些智能指针例如unique_ptr一同使用时,确实达到了一种所有权移动的感觉。

大家想的都是差不多的嘛。

另外std::forward也是基于引用给出的工具。用来把数据按原样返回。左值引用归左值引用,右值的归右值。

智能指针

这个对我来说不是很常用.

std::unique_ptr是无法复制但是可以移动的智能指针,刚才已经说了.

std::shared_ptr是可以在不同代码或者线程间共享的指针,是和上面的相对而言的.不过它不保证其内部保管的对象也可以线程安全…这让我想起来rust的Arc<Mutex<...>>.

与shared_ptr配套使用的还有make_shared.

内存模型

说实话,这个大概才是重点,不过对我来说不常用.

这意味着c++标准库将提供无关平台的一系列线程操作,包括线程,锁,原子操作,异步等.

  • std::thread 来自标准库的线程
  • std::async c++也从c#那里搬来了async和await那套异步编程模型

可变参数

也不常用…

想知道在没有可变参数的时候,printf是怎么实现的吗?那个模板嵌套是真的恐怖.直到今天C++也有可变参数了.其语法上是...

这玩意是用在模板里的

1
2
3
4
template <typename... T>
void printf(const char *pattern, const T... args){
//bula bula
}

既然有了可变参数,就能有元组.与其同时而来的还有std::tie,用于解构元组.

1
tie(name, age)=make_tuple("Tiansuo Li",24);

C++17里,这种解构被进一步提升为结构绑定.

1
const auto [x, y]=make_tuple("Tiansuo Li",24);

和面向对象有关的

目前又是不常用的东西.

显式方法重载.这个是面向对象里的概念.具体就是在重写虚函数时在函数签名最后注明override,编译器就会根据实际情况给出更多的提示和帮助.

final封闭方法.这个也是面向对象概念.用于将一个虚函数封闭,不再让子类重写.

=default默认方法.这个仍然是面向对象概念.显式的写明使用默认实现.

=delete删除方法…还是.显式的注明从类中删除某个方法(例如默认方法),防止他人调用.

允许非静态成员变量自行初始化.

方法重载能够注明左右值的区分.

array

不可变长的数组,需要在声明时就给出大小.这玩意很常用所以也搞出来.

与此相关的信息是vector的空间占用,这个东西的行为有如下两个特点

  • 当需要新空间时,将重新申请原来两倍的空间.
  • 将数据复制到新空间.

如果确定数据的大小,array显然是个更好的选择.

std::optional

天 下 大 同.

这玩意和rust的Option是一个东西.你家C++也要变成unwrap,unwrap,unwrap,unwrap,unwrap了.

Concepts

天 下 大 同 x 2.

这玩意用于限定模板中的类型.类似于rust内的+描述.

其他信息

  • std::chrono 来自标准库的时间计算
  • unordered_xxx 基于哈希的无序容器,这个已经很常用了吧
  • std::begin/end …这个算是填坑?虽然各个容器都提供了类似的API,不过在泛型编程时,还是需要一个这种东西.
  • 允许模板嵌套时右侧尖括号紧靠.这个其实很好.以及也要注意这个特性是在C++11里修订的.
  • noexcept 放在函数签名里表示不抛异常
  • attribute 用[[]]来标注属性(注解),有点像rust的#[[]]一类的东西.在C++20里,有一个比较有趣的注解likely可以向编译器建议分支的预测结果,这个和CSAPP课程相关性很强.
  • 二进制表示,在C++14中,这没啥可说的,0b010101.
  • 带初始化的if.在C++17中,能够用类似于golang中的带初始化if了.
  • 带初始化的rangefor.在C++20中,for(:)的玩意能够自带初始化语句了.
  • utf-8支持.在C17中,字符前缀u8能够获得utf-8的字符了.在C20中,存在char8_t用于支持字符串.

可能关注的其他东西

popcount.C++20提供的函数,可以数二进制数中1的个数.另外还有一些操作,位于<bit>中.

数学常数.例如std::numbers::pi.不过这个倒无所谓…应该都知道acos(-1).

C++走在现代化的路上

先提一个问题.GCC和Clang二者最新版本的编译器的安装可能会给你带来麻烦,尤其是Windows用户.MinGW,MinGW-w64,TDM-GCC都没有更新到最新版本.如果希望使用20标准,似乎最简单的办法是安装msys2.

其实在翻看完这些新标准后,我越发觉得我原来学的C和现在的C是不同的语言.你可以把C++当C来写,也可以停在11之前的标准来写,也可以大踏步来到20.三种风格写出来的代码完全不是一个东西.

另外,我一直没有注意智能指针,右值引用和移动概念,在了解Rust之后才意识到它们的重要性.除了没有Rust与生俱来的生命周期以及所有权的静态检查,C把其他该有的似乎全有了.倒不如说Rust也从C这里借鉴了大量东西,比如智能指针.

说实话,在此时我有点更倾向于使用C++了.

C++的编译器也能在移动语义上进行较为宽松的静态检查,such as 已经move的变量再次使用会被warning.


C++新标准特性初探
https://blog.chenc.me/2020/02/23/new-standard-of-cpp/
作者
CC
发布于
2020年2月23日
许可协议