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

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

noexcept修饰符与noexcept操作符

noexcept形如起名,表示其修饰的函数不会抛出异常(在C++11中如果noexcept修饰的函数抛出了异常,编译器可以选择直接调用std::terminate()函数来终止程序的运行),有2种语法形式:

一种就是简单的在函数声明后加上noexcept关键字,比如:

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

另外一种则可以接受一个常量表达式(结果会被转换成一个bool类型的值,true表示不会抛出异常,反之有可能抛出异常)作为参数,如下所示:

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

而noexcept作为一个操作符时,通常可以用于模板。比如:

 

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

以前的C++代码中,异常特性就很少被使用,因此noexcept需要被用到的场景也很少。但是有一个地方强烈建议使用:自定义类型的移动构造函数和移动赋值操作符。因为部分STL容器会判断:如果元素的移动构造函数和移动赋值操作符并非noexcept,将不会调用,而是继续用老的复制对象方式,导致性能上的损失。

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

decltype关键字

decltype实际上有点像auto的反函数,auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到类型。

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

一般的代码很少需要用到decltype,但是也有不得不用的场景:若要获取一个lamda表达式的类型,由于其类型不存在名字,唯一的方法只能是用decltype。另外在一些模板场景下也有用处。

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

static_assert关键字

static_assert提供一个编译时的断言检查。如果断言为真,什么也不会发生。如果断言为假,编译器会打印一个特殊的错误信息。

相对于以前的运行时检查assert,static_assert更不容易被误用,也没有什么安全风险(因为运行时什么也不会干)。建议所有在编译期能够检查出来的错误都用static_assert来检查。

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

变长参数的模板

以前的C/C++允许函数参数为变长,但是可选参数都是没有类型检查的,这导致了一大类运行时问题——比如把自定义结构体传给printf打印。现在,C++允许模板参数也为变长,和函数不同的是,每个模板参数都是有类型检查的。

一个使用变长参数模板的例子是Print函数,在C语言中printf可以传入多个参数,在C++11中,我们可以用变长参数模板实现更简洁的Print:

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

变长参数的模板在普通场景下较少需要用到。对于打印输出来说,更好的做法是彻底弃用printf这类函数而改用std::ostream。另外,对于二进制文件大小敏感的场景,需要注意变长参数的模板会带来模板膨胀问题——任一个参数不同都会生成一个新的函数/类型实例。

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

继承构造函数和委托构造函数

继承构造函数允许子类声明一个和父类一样的构造函数;委托构造函数允许不同参数类型的构造函数之间进行复用。这两个特性需要被使用的场景并不多见,以前程序员普遍习惯了通过父类构造函数显式调用和缺省参数来达到同样的效果,很难说新特性写法的可读性是否更好。

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

显式转换操作符

显式转换操作符限制自定义的类型转换操作符不能用于隐式转换,通常情况下应用场景有限。

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

非静态数据成员的类定义内初始化

C++11允许直接在类定义内来初始化成员变量。

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

相比起原来只能在构造函数中初始化的方法,直接在类定义中给出初始化值的方法有着更好的可读性,值得推广。

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

原生字符串字面量

原生字符串字面量(raw string literal)使得用户书写的字符串“所见即所得”,不需要如'\t'、'\n'等控制字符来调整字符串中的格式。C++11中引入了原生字符串字面量的支持,声明相当简洁,程序员只需要在字符串前加入前缀,即字母R,并在引号中使用括号左右标识,就可以声明该字符串字面量为原生字符串了。

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

而对于Unicode的字符串,也可以通过相同的方式声明。声明UTF-8、UTF-16、UTF-32的原生字符串字面量,将其前缀分别设为u8R、uR、UR就可以了。不过有一点需要注意,使用了原生字符串的话,转义字符就不能再使用了,这会给想使用\u或者\U的方式写Unicode字符的程序员带来一定影响。

此外,原生字符串字面量也像C的字符串字面量一样遵从连接规则。

原生字符串在一些需要大段字符串常量的场景下很有用,比如当测试代码中要写一个很长的JSON字符串,用原生字符串就会比原来方便很多。

相关链接:https://en.cppreference.com/w/cpp/language/string_literal

C++11新特性(标准库)

unordered_set和unordered_map

C++11中出现了两种新的关联容器:unordered_set和unordered_map,其内部实现与set和map大有不同,set和map内部实现是基于RB-Tree,而unordered_set和unordered_map内部实现是基于哈希表(hashtable),unordered_set和unordered_map内部实现的公共接口大致相同。

unordered_set和unordered_map是基于哈希表,因此要了解unordered_set/unordered_map,就必须了解哈希表的机制。哈希表是根据关键码值而进行直接访问的数据结构,通过相应的哈希函数(也称散列函数)处理关键字得到相应的关键码值,关键码值对应着一个特定位置,用该位置来存取相应的信息,这样就能以较快的速度获取关键字的信息。面对哈希冲突时,unordered_set/unordered_map内部解决冲突采用的是——链地址法,当用冲突发生时把具有同一关键码的数据组成一个链表。

在一个unordered_set/unordered_map内部,元素不会按任何顺序排序,而是通过元素值的hash值将元素分组放置到各个槽(Bucker,也可以译为“桶”),这样就能通过元素值快速访问各个对应的元素(均摊耗时为O(1))。

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

C++标准库中没有提供hash表容器一直是一个被广为诟病的问题。hash表在当今的软件系统中有着非常广泛的用途,很多项目组都需要它来达到最好的查找性能。过去,大多数程序员通过set、map来达成类似的效果,但实际上它们的查找性能和hash表相比有着显著的差距。从C++11开始,hash表正式进入了标准库,各个项目也应当大力推广使用。

虽然unordered_set和unordered_map具有优秀的性能,但是对于用户自定义类型作为key的场景,如何实现恰当的hash函数是个不可忽视的技术活。对于set/map这种二叉树结构来说,用户自定义类型只要按固定范式来实现operator<就可以了。但hash函数可远不像operator<这么好实现。有些程序员觉得只要把所有成员的hash值加起来就可以了,这是种幼稚的实现,这种方法产生的hash容器性能会很差。

关于hash函数如何实现的讨论,可以参考这个页面:https://*.com/questions/17016175/c-unordered-map-using-a-custom-class-type-as-the-key

鉴于hash函数的实现太过于考验程序员的功力,建议各项目组在使用hash表时,由项目统一提供辅助hash函数工具,不要让每个程序员自己去实现。

相关链接:https://zh.cppreference.com/w/cpp/container/unordered_set
https://zh.cppreference.com/w/cpp/container/unordered_map

std::tuple

类模板std::tuple是固定大小的异类值汇集,它是std::pair的推广。

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

在没有std::tuple时,要表示一个聚合就只能自定义一个结构体(或类)。std::tuple提供了另一种选择,可以在不额外定义类型的情况下表示任意几种类型的聚合。当然,在什么场景下用哪种方案更好还需要程序员自己决定。

相关链接:https://zh.cppreference.com/w/cpp/utility/tuple

std::tie

std::tie通常结合std::pair或std::tuple来使用。以前,C++一个函数要返回多个值,或者将多个值一对一的赋值/比较都是很麻烦的事,std::tie可以很有效的简化代码。

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

建议在新代码中,遇到自定义类型重载<运算符,以及临时解析std::pair、std::tuple内容时,都用std::tie来简化代码。

相关链接:https://zh.cppreference.com/w/cpp/utility/tuple/tie

std::array

std::array 是封装固定大小数组的容器。此容器是一个聚合类型,其语义等同于保有一个C风格数组T[N]作为其唯一非静态数据成员的结构体。不同于 C 风格数组,它不会自动退化成 T* 。

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

std::array是个值得大力推广用来替代数组的特性。相比于数组,其at成员函数带有越界检查,可以杜绝一大类数组越界隐患(但是要注意真的越界后产生的std::out_of_range异常的处理方式)。另外,std::array不能自动转换成指针的特点也让它避免了被开发人员滥用,并杜绝了数组形参退化成指针导致的一大类常见问题。back、fill、swap等成员函数比起手写的数组操作也更加简洁。

相关链接:https://zh.cppreference.com/w/cpp/container/array

std::bind

很多算法接口要求提供一个函数对象,但程序员手头有可能只有一个参数并不匹配的现成函数,这时可以通过函数适配器或者lamda表达式来进行两个接口之间的适配。在过去,C++提供了std::bind1st,std::bind2st等函数来指定绑定哪一个参数。但显然这些工具用起来太麻烦。C++11废除了以前这些适配器,提供了一个统一的bind函数模板,可以自动的根据传入参数类型和个数决定如何来适配。

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

std::bind相对于lamda表达式孰优孰劣就很难说了。如果函数的参数过多或重排序很复杂,_1、_2、_3这一堆占位符写在代码里恐怕算不上可读性多好。因此,建议最好只在参数绑定较简单的场景下使用std::bind。

相关链接:https://zh.cppreference.com/w/cpp/utility/functional/bind

Smart Pointers(智能指针):unique_ptr、shared_ptr、weak_ptr

现在能使用的,带引用计数,并且能自动释放内存的智能指针包括以下几种:

unique_ptr: 如果内存资源的所有权不需要共享,就应当使用这个(它没有拷贝构造函数),但是它可以转让给另一个unique_ptr(存在move构造函数)。
shared_ptr: 如果内存资源需要共享,那么使用这个(所以叫这个名字)。
weak_ptr: 持有被shared_ptr所管理对象的引用,但是不会改变引用计数值。它被用来打破依赖循环(想象在一个tree结构中,父节点通过一个共享所有权的引用(shared_ptr)引用子节点,同时子节点又必须持有父节点的引用。如果这第二个引用也共享所有权,就会导致一个循环,最终两个节点内存都无法释放)。
另一方面,auto_ptr已经被废弃,不会再使用了。

什么时候使用unique_ptr,什么时候使用shared_ptr取决于对所有权的需求,建议阅读以下的讨论:
http://*.com/questions/15648844/using-smart-pointers-for-class-members

以下第一个例子使用了unique_ptr。如果你想把对象所有权转移给另一个unique_ptr,需要使用std::move。在所有权转移后,交出所有权的智能指针将为空,get()函数将返回nullptr。

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

第二个例子展示了shared_ptr。用法相似,但语义不同,此时所有权是共享的。

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

第一个声明和以下这行是等价的:

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

make_shared<T>是一个非成员函数,使用它的好处是可以一次性分配共享对象和智能指针自身的内存。而显式的使用shared_ptr构造函数来构造则至少需要两次内存分配。除了会产生额外的开销,还可能会导致内存泄漏。在下面这个例子中,如果seed()抛出一个错误就会产生内存泄漏。

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

如果使用make_shared就不会有这个问题了。第三个例子展示了weak_ptr。注意,你必须调用lock()来获得被引用对象的shared_ptr,通过它才能访问这个对象。

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

如果你试图锁定(lock)一个过期(指被弱引用对象已经被释放)的weak_ptr,那你将获得一个空的shared_ptr。

智能指针可以在一定程度上对防范内存问题有帮助,但并不是万能的。智能指针如果使用不当,仍可能出现内存泄露、重复释放等各种问题。对于需要限制对象生命周期在某个局部作用域的场景,unique_ptr是个很好的选择,以前的auto_ptr应当被废弃。而对于对象生命周期超出局部作用域的场景来说,在设计上限定“谁申请,谁释放”很可能是比shared_ptr更好的解决方案。因为即使用了shared_ptr,也很难预知对象什么时候被释放,是否所有使用的代码都严格按规则使用,还是需要大量人工检查。而且,如果shared_ptr出了问题,有可能比以前更难定位。

相关链接:https://zh.cppreference.com/w/cpp/memory/unique_ptr
https://zh.cppreference.com/w/cpp/memory/shared_ptr
https://zh.cppreference.com/w/cpp/memory/weak_ptr

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

返回类型推导

C++11允许lambda函数根据return语句的表达式类型推断返回类型,C++14为一般的函数也提供了这个能力。C++14还拓展了原有的规则,使得函数体并不是{return expression;}形式的函数也可以使用返回类型推导。

为了启用返回类型推导,函数声明必须将auto作为返回类型,但没有C++11的后置返回类型说明符:

 

如果函数实现中含有多个return语句,这些表达式必须可以推断为相同的类型。

使用返回类型推导的函数可以前向声明,但在定义之前不可以使用。它们的定义在使用它们的翻译单元(translation unit)之中必须是可用的。

这样的函数中可以存在递归,但递归调用必须在函数定义中的至少一个return语句之后:

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

返回类型推导虽然方便,但对于一般的函数来说为了可读性不建议随意使用。而在模板编程中,有些场景下使用返回类型推导可以带来很大的方便。

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

decltype(auto)

C++11中有两种推断类型的方式。auto根据给出的表达式产生具有合适类型的变量。decltype可以计算给出的表达式的类型。但是,decltype和auto推断类型的方式是不同的。特别地,auto总是推断出非引用类型,就好像使用了std::remove_reference一样,而auto&&总是推断出引用类型。然而decltype可以根据表达式的值类别和表达式的性质推断出引用或非引用类型:

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

C++14增加了decltype(auto)的语法。允许auto的类型声明使用decltype的规则。也即,允许不必显式指定作为decltype参数的表达式,而使用decltype对于给定表达式的推断规则。

decltype(auto)的语法也可以用于返回类型推导,只需用decltype(auto)代替auto。

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

decltype(auto)可以解决特定场景下的问题,在一般场景下用auto就足够了。

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

放开constexpr限制

在C++11之后,编译期的数值计算可以通过使用constexpr声明并定义编译期函数来进行。相对于模板元编程,使用constexpr函数更贴近普通的C++程序,计算过程显得更为直接,意图也更明显。但在C++11中constexpr函数所受到的限制较多,比如函数体通常只有一句return语句,函数体内既不能声明变量,也不能使用for语句之类的常规控制流语句。

C++14解除了对constexpr函数的大部分限制。在C++14的constexpr函数体内我们既可以声明变量,也可以使用goto和try之外大部分的控制流语句。

虽说constexpr函数所定义的是编译期的函数,但实际上在运行期constexpr函数也能被调用。事实上,如果使用编译期常量参数调用constexpr函数,我们就能够在编译期得到运算结果;而如果使用运行期变量参数调用constexpr函数,那么在运行期我们同样也能得到运算结果。

准确的说,constexpr函数是一种在编译期和运行期都能被调用并执行的函数。出于constexpr函数的这个特点,在C++11之后进行数值计算时,无论在编译期还是运行期我们都可以统一用一套代码来实现。编译期和运行期在数值计算这点上得到了部分统一。

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

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

变量模板

变量模板是C++14的一个新的语法特性。C++新标准引入变量模板的主要目的是为了简化定义(simplify definitions)以及对模板化常量(parameterized constant)的支持

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

变量模板是个较为生僻的特性,在一般的场景下较少会用到。

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