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

二进制数字和数字分隔符

除了原有的十进制、十六进制和比较不常用的八进制表示方法之外,C++程序员现在还可以使用二进制表示常量了。二进制常量以前缀0b(或0B)开头,二进制数字紧随其后。

在英美两国,在写数字时,我们习惯于使用逗号作为数字的分隔符,如:$1,000,000。这些数字分隔符纯为方便读者,它提供的语法线索使我们的大脑在处理长串的数字时更加容易。

基于完全相同的原因,C++标准委员会为C++语言增加了数字分隔符。数字分隔符不会影响数字的值,它们的存在仅仅是为了通过分组使数字的读写更容易。

使用哪个字符来表示数字分隔符呢?在C++中,几乎每个标点字符都已经有特定的用途了,因此并没有明显的选择。最终的结果是使用单引号字符,这使得百万美元在C++中写作1’000’000.00。记住,分隔符不会对常量的值有任何影响,因此,1’0’00’0’00.00也是表示百万。

下面是一个结合了这两种新特性的例子:

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

可以看到,当二进制数字不长的时候,使用二进制字面量和分隔符可以有效地改善代码可读性,值得推广。但如果是很长的二进制数字,有可能用16进制会显得更简短一些。

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

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

std::make_unique

C++11标准中有std::make_shared可以让智能指针shared_ptr的初始化和对象构造在一条语句中完成,现在std::make_unique也被加入了标准,让unique_ptr也可以用同样的方式初始化。

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

相比原来的构造方式,std::make_unique显然要更加安全,应当要求新代码使用这种新的初始化方式。

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

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

Structured Binding(结构化绑定)

C++11提供的std::tie虽然好用,但是只能用于已经被声明的变量。有时为了解析一个函数返回的元组,不得不先声明很多个临时变量。结构化绑定允许用auto []自动声明很多个对应类型的变量,大大简化代码:

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

结构化绑定可以被用于std::pair、std::tuple、初始化列表(std::initializer_list)、数组、只包含简单成员的结构体、以及所有定义了get<N>的自定义类型。灵活使用结构化绑定,可以让C++代码看起来像动态类型语言(如Python)。

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

结构化绑定让代码更简洁,值得推广。

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

Init Statement for if/switch(if/switch语句中的初始化语句)

我们知道C++的for语句头部是可以声明变量的,该变量作用域只限定于for循环体内。现在if/switch语句也支持同样的用法了,这可以有效地避免临时变量之间重名,对于判断条件包含函数返回值时尤其有用。

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

当和结构化绑定结合使用时,这个特性能发挥更大的威力:

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

这个特性减少了代码中的命名个数,增加了可读性,值得推广。

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

Inline Variables(内联变量)

C++11的constexpr允许类的静态成员常量直接在类定义内部初始化,但是对于静态成员变量就不行了。内联变量特性允许非常量也在头文件中初始化,编译器会保证各个翻译单元看到的都是同一个变量实体。

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

需要用到这个特性的场景不多,很难说把静态成员初始化写在头文件中是不是一个值得推荐的做法。一旦遇到头文件不对齐,可能会造成一些相当麻烦的错误。但对于纯头文件的工具库来说,它还是很有用的。

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

Nested namespace definition(嵌入名字空间定义)

原来对于多层嵌套的名字空间定义必须写多层大括号,有了这个特性,现在可以这样写了:

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

这是一个影响不大的特性,有了它,在少数场景下可以简化一些代码。

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

constexpr if-statements(静态条件语句)

这个特性可以让编译器在编译期就决定哪个分支被运行,去掉运行时的判断开销。比如下面这个get_value函数模板,可以在编译期确定对于指针返回其指向的值,对于非指针直接返回其本身。

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

以前没有constexpr if时,用户需要使用函数重载和模板技巧来达成同样的效果,可能会借助C++11中的std::enable_if。现在使用constexpr if,可以让代码看起来更直观、容易理解。下面这段代码,用一个模板函数完成了以前需要写多个模板特例化才能完成的成员选择功能:

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

不过,constexpr if也带来了一个具有争议的问题:它是否增加代码的圈复杂度?以前在编译期选择分支通常用#ifdef预处理指令,它是不会被算作圈复杂度的。但是constexpr if就难说了。从可读性上讲,constexpr if确实和普通条件分支一样需要阅读者理解其判断过程,只不过没有运行时的开销。因此,对于是否应该全面推广使用constexpr if,很难给出统一的建议,只能留待开发人员根据具体场景来选择。

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

Template Argument Deduction for Class Templates(类模板的模板参数推导)

在以前,函数模板可以根据传入参数的类型自动实例化,但类模板在使用时必须要显式指定模板参数类型。C++17修改了模板推导规则,允许类模板也根据构造函数的参数类型自动实例化。

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

有了这个特性,很多的辅助函数模板可以省掉了,比如std::make_pair、std::make_tuple(当然,对于标准库中已有的辅助函数,继续用它们也没坏处)。以后使用类模板只需直接调用构造函数就可以。

但是需要注意的是,对于智能指针,辅助函数(std::make_unique、std::make_shared)可不能省,它们的作用并不只是用来推导类型,还能保证对象构造和指针初始化同时进行。

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

Non-type Template Parameters with auto(auto关键字用于非类型模板参数)

以前模板的非类型参数需要显式的写出类型(一般是int或者bool),现在允许让编译器来自动推导了。

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

非类型参数的类型变化的场景并不多,所以这个特性能派上用场的地方也有限。一般来说还是推荐写出确定的类型。

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

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

std::string_view

在很多编程语言中,字符串都是不可修改(immutable)的,这样就可以放心的让同一个字符串内容被多处引用共享。但是C++的std::string却是自带独立内存空间的可修改容器,这使得在某些场景下性能会劣化。比如,想要函数返回一个字符串,很难避免return时它被复制一次。当然,C++11以后我们可以用右值引用等方法来做一定程度的优化,但还是很难解决多处共享同一个字符串等场景的问题。

std::string_view为此而诞生,它不会对字符串内容进行任何修改,且没有内存管理的功能,仅仅只是将C的原生字符串做了一下包装。并且带有查找、越界检查、去除前缀/后缀、运算符重载等方便的功能。

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

由于实际代码中很多字符串是并不打算被修改的,std::string_view是一个非常值得推广的价值特性。相比于C原生字符串那些难以受控的指针操作,使用std::string_view能使代码更简洁、更安全、更易理解,同时也没有性能的下降。但要注意的是,由于std::string_view不负责做任何内存管理,而且允许很多个std::string_view共享同一片内存,程序员必须注意内部字符串空间的释放,以防止内存泄露和访问野指针。

相关链接:https://zh.cppreference.com/w/cpp/string/basic_string_view

std::optional

程序员经常碰到的一个问题是:一个函数有可能返回一个对象,也有可能返回失败,这种函数如何声明?以前通常有两种办法:1、函数返回错误码,同时用一个引用参数来带出对象。这种方式给调用者带来的麻烦是,必须要多写一行代码声明一个对象用于接收返回值。2、函数返回一个对象的指针,如果为空指针则表示失败。这种方法的一大隐患是对象的内存由谁来释放?稍有不慎就可能出现内存泄露或者重复释放。

C++17的标准库给出了一种更好的解决方案:std::optional模板。函数返回std::optional时,表示既有可能含值也有可能不含值。调用者可以用bool表达式来判断其是否含值,也可以用value_or函数来提供一个当不含值时的默认值。内部对象的生命周期由std::optional来管理,当std::optional对象被释放时其内部管理的对象也自动被释放。

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

std::optional可以很有效的简化代码,并防止安全问题,在写查找功能的函数时尤其有用。但是使用std::optional时也要注意在构造时如何减少拷贝开销,一般可以使用std::make_optional或者常量表达式来完成构造。另一方面,由于std::optional析构时会自动把内部对象也给析构掉,使用者应注意不要访问野指针。

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

std::variant

C++中的标准库容器都要求所有内部元素类型是一致的,但是实际开发中经常碰到需要把多个不同类型对象放到同一个容器中管理的需求,比如一个JSON对象中的value元素,既有可能是整型也有可能是浮点型。之前对这种场景有两种解决方案:1、对所有元素类型定义一个共同基类,容器中存放基类的指针。这是经典的面向对象多态设计方法,但在C++语言中的问题是这样做以后对象的内存就只能手工管理,得手写代码在合适的时期new和delete。2、容器中只存放无类型的内存(char*或者void*),由使用者根据上下文信息来将其转换为合适的对象类型。这种方法是不推荐的,一旦类型转换错误,会出现难以定位的运行时错误。

C++17提供了一种新的工具:std::variant。它允许多种不同的可能类型存放于同一个std::variant对象中,但同一时间内std::variant只会有某一种类型的对象。使用者可以通过index函数来检查当前类型,也可以用std::get指明类型序号或者类型名来获取其中的对象。std::variant对类型有强校验,当类型不符合时会触发编译错误或者抛出异常。

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

std::variant对于不希望手工管理生命周期的多类型小对象来说是个很好的选择。使用std::variant并不会妨碍多态的优点,程序员仍然可以通过基类对象引用来获取std::variant的内部对象(自动upcast)并对其执行虚函数。

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