现代C++语言(C++11/14/17)特性总结和使用建议(一)

 

C++语言在历史上经过了很多次的演进。最早的时候,C++语言没有模板、STL、异常等特性,之后加入这些特性形成大多数人所熟悉的C++98/03标准。在此之后,C++经过10多年又孕育出了拥有众多革命性变化的C++11标准(在标准正式发布前,被称为C++0x)。C++11包括了约140个新特性和约600个缺陷的修正。由于其变化实在太大,被很多人称为“现代C++语言”(Modern C++ Language),以和之前的经典C++语言作区分。在C++11后,标准委员会又发布了C++14和C++17标准,其内容大多是对C++11特性的一些修改和补充。

业界关于C++11/14/17新特性的书籍已有不少,网上的介绍文章更多。但是这些书籍和文章都只是对其中的一部分特性作有挑选的介绍。对于现代C++这种内容庞杂的语言改进来说,也不可能通过某一本书进行完全覆盖。开发人员在实际编程中碰到特定特性的问题,还是需要靠网络搜索来学习和解决。

本文试图从我司实际角度来阐述C++新特性所带来的影响,主要目标针对我司的存量产品嵌入式开发领域,挑选一些开发人员应当主动了解并在项目中多加使用的特性进行说明,并且尽可能的给出其他外部资源的链接,达到“授之以渔”的目的。

另一方面,现代C++语言的新特性虽然广受赞誉,但如果使用不当,也会掉入新的陷阱,产生令人头疼的问题,而这些陷阱却很少有文章提起。本文试图从个人经验的角度,多描述一下实际使用建议和注意事项。

C++11/14/17新特性概述及编译器支持

首先,哪些编译器支持哪些新特性?这个问题可不好回答。我们没有办法笼统地说“xxx版本的编译器支持C++11”或“xxx版本的编译器支持到C++14”。因为,每个编译器版本都是选择对部分特性的支持,甚至对单个特性,也可能只是支持其一部分场景。比如,常用的gcc 4.9版本,不光支持大部分的C++11特性,也支持一部分C++14特性,甚至还提前支持了个别C++17和C++20特性。因此要深入了解某特性支持状况,需要查询相关资料。

这里给出一个非常全面的主流编译器支持C++特性现状列表,地址如下:
https://en.cppreference.com/w/cpp/compiler_support

这个页面有很多好处:每个特性都给出了标准建议文档的链接,有些还包括在线的特性说明页面链接,可以方便的跳转到相应内容来进一步的了解语言特性。建议大家收藏这个页面供日后查询。

下文在介绍各个特性时,努力把更常用的特性放在前面。但是,由于各个项目的业务背景差别,在应用语言特性的需求上肯定大不一样,因此大家可以挑选自己感兴趣的内容重点阅读。

本文分开介绍C++核心语言特性和标准库特性,但标准库(STL)的很多改进同样是非常重要和常用的,不应当被忽视。

C++11新特性(语言核心)

auto关键字

在C++11之前,auto关键字用来指定存储期。在新标准中,它的功能变为类型推断。auto现在成了一个类型的占位符,通知编译器去根据初始化代码推断所声明变量的真实类型。各种作用域内声明变量都可以用到它。例如,名字空间中,程序块中,或是for循环的初始化语句中。

 

现代C++语言(C++11/14/17)特性总结和使用建议(一)

使用auto通常意味着更短的代码(除非你所用类型是int,它会比auto少一个字母)。试想一下当你遍历STL容器时需要声明的那些迭代器(iterator)。现在不需要去声明那些typedef就可以得到简洁的代码了。

 

现代C++语言(C++11/14/17)特性总结和使用建议(一)

需要注意的是,C++11中的auto不能用来声明函数的返回值。但如果函数有一个尾随的返回类型时,auto是可以出现在函数声明中返回值位置。这种情况下,auto并不是告诉编译器去推断返回类型,而是指引编译器去函数的末端寻找返回值类型。在下面这个例子中,函数的返回值类型就是operator+操作符作用在T1、T2类型变量上的返回值类型。(C++14改进了这些限制)

 

现代C++语言(C++11/14/17)特性总结和使用建议(一)

auto特性可以在很大程度上简化代码,节约编程人员的体力,而且不容易出现错误,因此应当大力提倡使用。

需要注意的有两点:第一是如果想要声明一个引用类型,必须用auto&,同理,对于指针、const、volatile等修饰符也可以用在auto上进一步限定类型。第二是C++11不允许直接用auto声明函数返回值,但C++14做了改进,可阅读相关资料了解。

相关链接:https://zh.cppreference.com/w/cpp/language/auto

nullptr关键字和std::nullptr_t类型

以前都是用0来表示空指针的,但由于0可以被隐式类型转换为整型,这就会存在一些问题。关键字nullptr是std::nullptr_t类型的值,用来指代空指针。nullptr和任何指针类型以及类成员指针类型的空值之间可以发生隐式类型转换,同样也可以隐式转换为bool型(取值为false)。但是不存在到整型的隐式类型转换。

 

现代C++语言(C++11/14/17)特性总结和使用建议(一)

nullptr可以有效地减少二义性,防止错误,应当大力鼓励使用。需要注意的是,由于以前的NULL仍然是有效的,而且有些历史代码中存在把NULL当作整型数字0的不良习惯,应当要求所有新代码都只用nullptr,彻底废除NULL,不要两者混用。

相关链接:https://zh.cppreference.com/w/cpp/language/nullptr

Range-based for loops(基于范围的for循环)

为了在遍历容器时支持“foreach”用法,C++11扩展了for语句的语法。用这个新的写法,可以遍历C类型的数组、初始化列表以及任何重载了非成员的begin()和end()函数的类型。

如果你只是想对集合或数组的每个元素做一些操作,而不关心下标、迭代器位置或者元素个数,那么这种foreach的for循环将会非常有用。

 

现代C++语言(C++11/14/17)特性总结和使用建议(一)

基于范围的for循环已经在其他语言中被广为接受,相比于原来手工递增/递减循环变量的做法,可以减少越界、溢出、意外修改循环变量等很多问题,应该大力推广替代老式的for循环。需要注意的是,和以前一样,在循环内部严禁修改正在被遍历的容器大小(比如在循环内删除元素)。另外,对于自定义的容器类,都应该显式定义begin和end函数让其可以被用于新式的for循环。

由于新式的for循环内部也是会给声明的变量赋值的,因此某些性能方面的考虑仍然值得注意。比如,如果不希望拷贝容器内部的元素,那么在声明工作变量时应当用auto&而不是auto。

相关链接:https://zh.cppreference.com/w/cpp/language/range-for

constexpr(编译期常量类型)

C++11中对编译时期常量的回答是constexptr,及常量表达式(const expression),例如:

即在函数表达式前面加上constexpr关键字即可。有了常量表达式这样的声明,编译器可以在编译时期对GetConst()表达式进行值计算(evaluation),从而将其视为一个编译时期的常量。在C++11中,常量表达式实际上可以左右的实体不限于函数,还可以作用于数据声明,以及类的构造函数等。

 

现代C++语言(C++11/14/17)特性总结和使用建议(一)

通常我们可以在函数返回类型前加入关键字constexpr来使其成为常量表达式函数。不过并非所有的函数都有资格成为常量表达式函数。事实上,常量表达式函数的要求非常严格,总结来说,大概有以下几点:
函数体只有单一的return返回语句。
函数必须有返回值(不能是void函数)。
在使用前必须已有定义。
return返回语句表达式中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式。(C++14放宽了这些要求)

以前的const修饰符并不能保证产生编译期常量,因此某些用法,比如用const常量声明数组长度不能保证可行。相比之下,constexpr类型具有更强的编译期检查,可以在编译阶段就发现常量能否在编译阶段确定。应当要求所有新代码中的编译期常量都声明为constexpr。

相关链接:https://zh.cppreference.com/w/cpp/language/constexpr

模板别名

在C++中,使用typedef为类型定义别名,比如:

 

现代C++语言(C++11/14/17)特性总结和使用建议(一)

在C++11中,定义别名已经不再是typedef的专属能力,使用using同样也可以定义类型的别名,而且从语言能力上看,using丝毫不比typedef逊色。

 

现代C++语言(C++11/14/17)特性总结和使用建议(一)

在上面的例子中,使用了C++11标准库的is_same模板类来帮助我们判断2个类型是否一致。在使用模板编程的时候,using的语法甚至比typedef更加灵活,比如:

现代C++语言(C++11/14/17)特性总结和使用建议(一)

 

在这里,我们“模板式”的使用了using关键字,将std::map<T, char*>定义为一个MapString类型,之后我们还可以使用类型参数对MapString进行类型的实例化,而使用typedef将无法达到这样的效果。

从功能和可读性上看,using模板别名都比原来的typedef要更好,因此新写的代码都应使用using来取代typedef定义。

相关链接:https://zh.cppreference.com/w/cpp/language/type_alias