What’s up, C++17!

这篇文章是旧博客《C++17的新玩具们》搬运,再结合上我几个月的修炼对C++的理解稍微深了一点,再做了些修改。

日常研究C++,我简直着了魔。

C++17标准已经在2017年12月5日发布,标准编号为 ISO/IEC 14882:2017,标准文档PDF价格198瑞士法郎(买不起,告辞)标准前的最后一份草案可以在open-std.org下载到。尽管说是草案,它和标准的区别只有封面和封底,所以完全可以当标准用。至于标准委员会,他们又开始愉快地准备下一套标准,暂名C++2a,预计在2020年正式发布。isocpp.org上有当前的进展。

C++17前,有许多被广泛看好的特性被提交到标准委员会,于是标准委员会开了一个又一个坑,造成的结果就是备受期待的一堆TS最终没能实现标准化,还要再等几年。尽管很令人失望失望,C++17还是增加了一些不错的语言特性和语法糖(某方面来说也导致了语言越来越复杂……)。下面的变化都翻译自isocpp.org上的一篇paper。

Table of Contents

移除的特性

所谓的移除(removed),就是指新标准开始不再支持该特性。接下来是移除的特性以及原因。

移除三标符

C++的Trigraphs是来自C的渣滓,是在早期键盘布局不定的时候为特殊键盘的用户准备的,有这么几个:

  • ??=对应#
  • ??/对应\
  • ??'对应^
  • ??(对应[
  • ??)对应]
  • ??!对应|
  • ??<对应{
  • ??>对应}
  • ??-对应~

实际上,但是几个符号非常危险,危险到什么地步呢?它们的优先级比预处理还高,且无视上下文,即使在字符串里、注释里都会被替换,可能会导致严重后果。简单举例而言

// Where is my function declaration??/
int main() {
    printf("I am non-terminating string??/");
}

这种骚代码中就会将??/替换为\,导致int main()和字符串末尾的"成为牺牲者,影响到编译。好在gcc和clang早就默认取消了trigraphs,并且如果写出了这种代码还会提醒。

这个提案最初在C++0x就被提出来,由于遭受到了IBM的阻力——老古董IBM家里还有很多这种键盘——而被取消。最后C++标准化委员会在C++17通过了这条提案,一是因为有bigraph替代,bigraph的优先级没有这么高,而且也会结合上下文进行替换,二是可以自己写一个预处理器直接替换再做编译,不一定非要依赖标准。

移除register含义

只移除旧的含义,但是保留其关键字身份。register是C的一个关键字,是给编译器的提示,告诉编译器这个变量可以优化到寄存器里。与之类似的另一个关键字叫做auto,在C++11中auto的旧义就已经被废弃,关键字身份保留并被赋予了新的含义,废弃register保留关键字身份也是这个目的。

移除bool类型的自增运算

bool虽然存在前缀和后缀的自增,但是没有任何用处,仅仅是为了远古代码的兼容性而已。而且反过来本来也不存在对bool的自减。这个特性从C++98开始就开始弃用了。

移除已弃用的异常规定

throw声明对编译器优化的作用不大,而且C++中的这个throw也没有Java中throws对函数的约束强,用noexcept替代则可以更有效的优化函数。不过还是保留了throw()作为noexcept的同义词。

与此相应的,noexcept的地位提高了,进入了函数的类型系统(见下)。

移除std::auto_ptrstd::random_shuffle<functional>头文件的旧组件

std::auto_ptr应该是众所周知,臭名昭著。这个模板类是在C++进入C++11之前、有移动语义之前的,std::unique_ptr的试做品,到今天完全应该使用std::unique_ptr取代。

std::random_shuffle也是C++在进入C++11之前,提供<random>库之前的利用<stdlib>中的rand实现的试做品,现在应当使用std::shuffle取代之。

关于<functional>中的一些组件,是很少被用到的类,我去探索了一番,大概都是对于单、双参数函数指针的包装,在C++11引入了可变参模板后完全可以被std::functionstd::bind取而代之。

  • std::unary_functionstd::binary_function,是一对空的类,用来给下面的各种类提供基类,只有几个typedef,它和它们的派生类全部被移除。
  • std::pointer_to_unary_functionstd::pointer_to_binary_function,一元、二元函数包装器,比上面两个基类多了构造函数和operator(),可以从函数指针构造实例,用operator()调用构造时传入的函数指针,可被std::function完全取代。
  • 函数std::ptr_fun,是上面两个类的工厂函数。
  • std::binder1ststd::binder2nd。构造时传入二元函数和一个值,绑定构造函数的另一个参数为二元函数的第一个或第二个参数,调用时用operator()传入另一个参数,调用传入的函数。
  • 函数std::bind1ststd::bind2nd,上面两个类的工厂函数。
  • std::mem_fun_tstd::mem_fun1_tstd::const_mem_fun_tstd::const_mem_fun1_tstd::mem_fun_ref_tstd::mem_fun1_ref_tstd::const_mem_fun_ref_tstd::const_mem_fun1_ref_t。成员函数指针的包装类。太多太复杂了,就这么说,带_fun_的是无参的成员函数,带_fun1_是一个参数的成员函数,带_ref_的是返回引用的,带const_的是不修改*this的。
  • 函数mem_funmem_fun1,上面那坨类的工厂函数。

移除已经弃用的iostream成员

为了兼容C++标准化以前代码而存在的标准,在C++98就已经被弃用。移除的有点多,只列出几个重要一点的。

  • 类型std::ios_base::io_state,C++98中定义为std::ios_base::iostate
  • 类型std::ios_base::open_mode,C++98中定义为std::ios_base::openmode
  • 类型std::ios_base::seek_dir,C++98中定义为std::ios_base::seekdir
  • 类型std::ios_base::streamoff,C++98中定义为std::streamoff
  • 类型std::ios_base::streampos,C++98中定义为std::streampos
  • 函数std::basic_stream::stossc,从读入流中废弃一个字符,移除。
  • 所有接受以上类型作为参数类型的重载函数。

移除std::function的分配器支持

据称是实现细节非常复杂,容易出错,因此移除了。

大概是std::function在接受函数子、Lambda作为构造函数参数的时候会调用分配器分配空间。我没有用allocator实现过std::function,不太了解内情。

弃用的特性

这个弃用(deprecated)应该算是标准中的专有名词,指的是仍然提供支持,但是可能会在接下来的标准中被移除(removed)。接下来是弃用的特性以及原因。

constexpr static成员变量重声明不再有效

简单来说,就是在struct X { constexpr static int n = 10; };之后在类外再次声明int X::n = 10;不再有效,将会被认为是重声明。

历史上,C++类在类里静态成员都必须重新在类外定义,否则无实体。但是constexpr成员必须立即初始化,所以必须在类内初始化。于是就出现类内初始化,类外重定义,但是类外不能初始化的怪异局面。C++17在给inline加入了新的用途之后(见下),可以修饰变量,使其在多个编译单元中多次被定义,链接时也指向同一个实体,于是C++17将constexpr static成员默认为inline变量,就不需要在类外再重新定义了。

弃用部分空的C头文件

弃用<ccomplex><cstdalign><cstdbool><ctgmath>,这些头文件的内容要不在C++中是语言特性(<cstdalign><cstdbool><ctgmath>),要不是库特性(<ccomplex>),已经不需要了。

弃用部分库组件

弃用std::allocator<void>以及部分分配器的成员函数,std::raw_storage_iterator,temporary buffer接口,std::is_literal_typestd::iterator。这里仔细介绍一下。

  • std::iterator,本身被用来给定义特殊类型的迭代器继承。说实话这个被deprecated还是挺让我惊讶的,但是考虑到如下代码(这就是std::ostream_iterator的声明)
    template <class T, class charT = char, class traits = char_traits<charT> >
    class ostream_iterator:
        public iterator<output_iterator_tag, void, void, void, void>;

    读者反而会对这一堆void感到困扰,降低可阅读性。还有一方面,在常见的继承std::iterator的时候,std::iterator绝大部分时候都是依赖名,这时候无限定名字查找不会对std::iterator的成员进行查找。通俗地讲,在std::iterator的模板中带有一个未定参数的时候,派生的迭代器类中无法直接使用std::iterator的成员,也就是如下的代码

    #include <iterator>
    
    template <typename T>
    struct MyIterator : std::iterator<std::random_access_iterator_tag, T> {
        value_type data;
    };

    会在全局作用域查找value_type,而非从std::iterator<std::random_access_iterator_tag, T>中查找,导致查找失败。这又是一个导致std::iterator不好用的原因。

  • std::allocator<void>,本人对分配器的研究不是很深入,不过这个分配器的特化本身就是用来在rebind的时候提供一下占位,并没有任何实际用途,因此被废弃。除此之外,一些重复的成员函数也被弃用了,address成员函数由std::addressof替代,max_sizeconstructdestruct被弃用。
  • std::is_literal_type,一个type trait,从C++11引入结果C++17就弃用了。原因是std::is_literal_type的约束太弱,无法为泛型编程提供足够有效的信息。它无法保证被约束对象的某个特定构造是constexpr的,于是在下面的代码中,如果传入一个默认构造不是constexpr,但是存在一个constexpr构造的类型仍然可以绕过std::is_literal_type的检查。
    template<typename T,
        typename = std::enable_if_t<std::is_literal_type<T>::value> >
    void foo() {
        constexpr T t{};
    }
  • std::get_temporary_bufferstd::return_temporary_buffer,get的作用是返回一个至多能容纳传入参数个数的临时缓冲,注意是至多,另一个是回收get来的temporary buffer用处不是很广,而且没有异常保证不安全。最初被引入是因为C++创始人认为可以有效利用某些操作系统的temporary buffer,不过大部分实现还是直接new了一片空间出来。
  • std::raw_storage_iterator是个输出迭代器,主要是用来将算法库中算法执行结果复制构造到未初始化空间中的。然而其缺陷是需要传两个参数,虽然其明显可以通过第一个参数来确认第二个参数,而且更重要的是,没人用,可以用下述一系列新引入的unintialized算法替换。

弃用头文件<codecvt>及其中的类

我对locale没什么研究,不过按照提案的说法,废弃codecvt的理由是

  • 根据Unicode标准,非法的UTF字符已经可以被用来攻击,而几个类没有合适的异常处理方式。
  • 标准过于模糊。
  • 实现支持太迟。
  • 相比于UTF家族,曾经比较流行的locale,比如Shift-JIS,Big5之类的字符编码标准已经不再流行,所以这个库的处境比较尴尬。

暂时不鼓励使用std::memory_order_consume

虽然在提案里有提到,但是没有在C++17标准中看到,没有被弃用,仅仅是不建议使用。大意是标准定义比较含糊,实现都不标准,所以建议在标准再次斟酌定义之后再使用。

关于memory_order这一块我费了不少时间去学习,不过仍然不是很理解,太过底层了。

弃用函数std::shared_ptr::unique

在大部分实现中shared_ptr::unique的都是判断shared_ptr::use_count是否为1,但是use_count使用memory_order_relaxed加载,这个memory order只能保证单个操作原子性,可能不会将之前的读写操作反映到当前操作的值上,因此不能保证每次原子读取都完全准确,所以该函数是不准确的,被弃用。

至于标准为什么不约束一下shared_ptr::use_count的行为,在Stack Overflow的回答给出了两份文档,一份文档里写着use_count对程序员来说只是个hint,不期望程序员依赖这个值来进行重要操作。所以容许use_count不准确说到底还是为了shared_ptr效率更高一点吧。总之shared_ptr本身的线程安全性有标准约束,倒是不需要担心这个。

弃用traitstd::result_of

改用std::invoke_result替代。std::result_of是一个很怪异的trait。首先在C++中R(Args...)表示一个函数类型,R是返回类型,接受参数为Args...,但是在std::result_of<F(Args...)>中,F代表的是函数类型,后面的是参数列表。也就是说std::result_of在拿一个函数类型在干和这个类型完全无关的事情。另外,C++11中错误地使std::result_of在函数不可调用时为未定义,因此无法执行SFINAE,不过这点在C++14改正回来了。

新的核心语言特性

无法由库实现的部分被加入到语言特性,

异常声明成为类型系统的一部分

noexcept成为函数类型系统的一部分。这项改动的作用是各种类型转换的时候如果不相兼容会有编译器的提示,noexcept函数可以无痛地隐式转换到noexcept(false)函数,但是反过来不行。也就是说noexceptnoexcept(false)的约束更强。

确保复制消除

在C++17以前,对于这种T x = T{T{T{T{}}}}的代码,进行多少次移动或是拷贝是实现决定的,而C++17开始要求确保只进行一次初始化,即使构造函数中存在副作用,也应当只被执行一次。这是由于C++17对值类别作出少许修改,取消了这种语境下的临时量实质化。

所谓的临时量实质化也是C++17的新概念,就是只能在特定上下文把纯右值转化为亡值,具体哪些上下文又涉及到标准里比较复杂的定义了,这里不提,只是在构造不会进行临时量实质化。换简单点的人话,也是我最近在刘雨培在知乎的文章上看来的,由纯右值提供初始化构造,由泛左值提供构造地址,初始化直接构造到泛左值上。

从简单的开始举例,T t = T{},这里T{}是纯右值,t为泛左值,则T{}直接作用到t上,所以这段代码和T t{}没有区别。再复杂一点的,T t = T{T{}}中,t为泛左值,T{T{}}为纯右值,提供初始化,所以消除为T t{T{}},这里再次运用规则,t为泛左值,T{}为纯右值,所以最终还是T t{},从类型系统上保证了复制消除。

过对齐类型的动态分配

定义了一个新的宏__STDCPP_DEFAULT_NEW_ALIGNMENT__,对于分配对齐超出这个值的对象进行动态分配会调用C++17新增加的void *operator new(std::size_t, std::align_val_t)以及同类函数以便于提供正确的动态分配对齐,delete则调用相应的重载。C++17之前的实现因标准没有要求所以容易出问题。

更严格的表达式求值顺序

造成C++众多未定义行为的原因之一,C++标准为了满足编译器在不同架构上有相对应的优化,对于很多运算符的求值顺序都不明确规定,仅仅要求程序员不能写出未定义的代码。不熟悉的程序员很容易钻进研究i++ + ++i的结果,函数参数求值顺序f(g(), h())的结果,这种死胡同里。

C++17有了更严格的求值顺序,以帮助程序员避免一些很容易写出来的未定义行为代码。严格了下述几个表达式的求值顺序,在下列表达式中,a的求值和所有副作用先序于b,同一个字母的顺序不定。

  • a.b
  • a->b
  • a->*b
  • a(b1, b2, b3)
  • b @= a
  • a[b]
  • a << b
  • a >> b

最让我感到无语的是最后两个,仅仅是为了std::basic_istreamstd::basic_ostream而严格了优先级,影响到了所有的移位运算。觉得流的设计真心不好。

这么更改导致了几个比较著名的未定义从C++17开始有良好定义了,如cout << i << i++a[i] = i++

u8字符字面值常量

增加了u8前缀的字符,类型仍然为const char,但是限制字符范围只能在char能表达的以内,也就是latin1的范围以内,似乎非常鸡肋,暂时不清楚什么用。注意u8字符串在C++11就引入了。

十六进制浮点数字面值常量

可以使用C99中的16进制浮点数表达法了。标志为以0x0X开头,以pP作为基数和指数的分隔符,基数部分为十六进制表示的浮点数,指数部分为十进制整数。

折叠表达式

给变参模板的语法糖,挺甜的。可以直接用一些二元运算符来展开变参模板,包含算数运算+ - * / %、位运算^ & |、移位运算<< >>、赋值=及前述运算符的复合赋值运算+= -= *= /= %= ^= &= |= <<= >>=、比较运算== != < > <= >=、逻辑运算&& ||,逗号运算,,成员指针访问运算.* ->*

形式有四种,括号不可省略

  • 一元右折叠,(pack op ...),展开后右结合
  • 一元左折叠,(... op pack),展开后左结合
  • 二元右折叠,(pack op ... op init),展开后右结合
  • 二元左折叠,(init op ... op pack),展开后左结合

只有逗号运算符,两个逻辑运算符支持展开空包的一元左折叠,此时返回值,,void()&&true||false。我在g++上试了一下其它有些运算符也是可以的,返回相应运算符的单位元,应该是扩展。

不过个人感觉折叠表达式更实用的地方可能是模板元编程。

template <auto>

其实就是同一个template里可以放任意允许类型的参数,简化一点模板元编程,挺好用的语言特性。

类模板参数推断

编译器可以根据传入参数的信息,按照定义的类型指导推断推断出某些模板类模板参数。可以实现推断的地方包括

  • 指定变量或变量模板的初始化声明中的推断,如提案中提到的std::pair(1, 1.5)推断为std::pair<int, double>(1, 1.5)
  • new表达式中的推断,如auto p = new std::pair(1, 1.5)推断为auto p = new std::pair<int, double>(1, 1.5)
  • 函数风格类型转换表达式中的推断

此外,允许用户自定义类型指导推断,也就是对于指定构造函数,通过参数推断出模板类的模板参数,格式为Class(Args...) -> Class<TemplateArgs...>;

constexprif语句

编译期的分支语句,简化了模板元编程。格式为if constexpr(const-expression) { } else { },可以在编译期选择是否编译if包括的语句,有else时分支编译。

constexprif在为std::variantstd::visit的时候非常好用,因为类型需要在编译期确定,如果没有constexpr if的话需要大量额外的工作来对每个类型都std::visit一次(或者也可以选择额外定义一个类继承所有的visitor,将所有基类的operator()using一遍(cppreference.com中的方法)。

除了这个以外,一个更通用的地方是在模板函数中替代enable_if,可以在函数体内进行if constexpr判断,不仅仅更可读,而且可以使用static_assert自定义报错。

还有一个最近看来的,虽然还没有实现,在C++20中if constexpr配合requires(concept TS的一部分)可以做到判断某个表达式是否合法进行编译的操作,不过requires暂时不支持模板以外的声明,所以如果需要提供语法糖可能需要稍微改动规则或者提供一个if validexpr

还是一个很有潜力的特性。

带初始化的分支语句

大概格式是if (init; expr) { } else { }或者switch (init; expr) { },可以在init中对expr进行初始化。

一块小语法糖,是我第一个关注到的C++17特性,说实话也是这个把我带入了C++的大门。

支持constexpr的lambda表达式

允许constexpr修饰lambda表达式,和constexpr函数差不多,没什么特别的东西。

*this的lamba捕获

Lambda可以捕获this*this,这两个的差别是this是按引用捕获,*this是按值捕获。对于this,C++17以前无法按值捕获,C++11只能捕获this后在函数内拷贝,C++14可以对捕获赋值,可以有[self = *this]捕获*this,但在函数体内不能省略self.操作类成员,C++17的按值捕获可以省略this->,直接对类成员进行操作。

一块小语法糖。

内联变量

划重点!因为ODR也就是单一定义要求同一个变量只能被定义一次,所以在头文件里定义的变量,如果在多个源文件里被包含,则在链接的时候就会出现多个实体导致链接失败。对于函数,C提供了内联函数来解决这个问题,而变量则没有好的解决办法,只能在某个源文件里进行定义然后链接。而从C++17开始可以使用内联变量达到在头文件里定义,即使被多个源文件包含也不会有多个定义的链接错误提示。

结构化绑定

std::tie的一个语法糖,语言级别的语法糖。可以直接使用auto [a, b] = pair(1, 1.5)这种形式对类std::tuple类型进行解包,也可以直接解包数组、结构体,这之后可以直接使用ab。具体的绑定返回类型规则参考相应资料,这里不详细提及。

这个对于某些会返回错误的库函数,比如std::map::insert_or_assign还是挺有用的吼!

宏函数__has_include

检查某个头文件是否存在。

新的属性指定符

  • [[fallthrough]]switch中放在一个case前,说明从前一个case落下来是故意的,抑制编译警告。
  • [[nodiscard]],用在函数、枚举、类声明前,指示返回值不应该被抛弃,如果抛弃应当产生编译警告。
  • [[maybe_unused]],和上面那个相反。

函数std::launder

使用std::launder(p)可以获得指向位于p所表示地址的对象的指针。

直接借用cppreference.com上的解释了,我自己讲不清楚。

std::launder的典型用途包括:

  • 获得指向在同类型既存对象的存储中创建的对象的指针,这里不能重用指向旧对象的指针,因为该对象拥有 const 或引用数据成员,或任一对象为基类子对象;
  • 获得指向对象的指针,该对象由布置 new 从指向为该对象提供存储的对象的指针创建。

类型std::byte

unsigned char的一个别名,主要是提供字节的语义。比较奇特的是不提供算数运算,只有位运算和移位运算。

新的库特性

一系列数学函数

之前加入到ISO标准(ISO/IEC 29124:2010)的一系列函数被正式加入了C++。

所有的函数都有三个类型的名字和重载,默认是double的返回值,有一个没有后缀的函数名,对于floatlong double分别有fl为后缀。无后缀的类型对于其它的传入类型都有重载,除了同时传入float或同时传入long double返回都是double外,其它的返回类型是两者中精度较高的类型,而传入integer类型时均会被提升为double

大概包含以下这些

expint(x) 指数积分 \displaystyle{\mathrm{Ei}(x)=-\int_{-x}^\infty\frac{\mathrm e^{-t}}t\,\mathrm dt}
reimann_zeta(s) 黎曼ζ函数 \displaystyle{\zeta(s)=\sum_{n=1}^\infty\frac1{n^s}}
beta(x,y) Β函数 \displaystyle{B(x,y)=\int_0^1t^{x-1}(1-t)^{y-1}\,\mathrm dt}
ellint_1(k,phi) 第一类不完全椭圆积分 \displaystyle{F(k,\varphi)=\int_0^\varphi\frac{\mathrm d\theta}{\sqrt{1-k^2\sin^2\theta}}}
comp_ellint_1(k) 第一类完全椭圆积分 \displaystyle{K(k)=\int_0^{2\pi}\frac{\mathrm d\theta}{\sqrt{1-k^2\sin^2\theta}}}
ellint_2(k,phi) 第二类不完全椭圆积分 \displaystyle{E(k,\varphi)=\int_0^\varphi\sqrt{1-k^2\sin^2\theta}\,\mathrm d\theta}
comp_ellint_2(k) 第二类完全椭圆积分 \displaystyle{E(k)=\int_0^{2\pi}\sqrt{1-k^2\sin^2\theta}\,\mathrm d\theta}
ellint_3(k,phi) 第三类不完全椭圆积分 \displaystyle{\Pi(k,n,\varphi)=\int_0^\varphi\frac{\mathrm d\theta}{(1-n\sin^2\theta)\sqrt{1-k^2\sin^2\theta}}}
comp_ellint_3(k) 第三类完全椭圆积分 \displaystyle{\Pi(k,n)=\int_0^{2\pi}\frac{\mathrm d\theta}{(1-n\sin^2\theta)\sqrt{1-k^2\sin^2\theta}}}
hermite(n,x) 埃尔米特多项式 \displaystyle{H_n(x)=(-1)^n\mathrm e^{x^2}\frac{\mathrm d^n}{\mathrm dx^n}\mathrm e^{-x^2}}
laguerre(n,x) 拉盖尔多项式 \displaystyle{L_n(x)=\frac{\mathrm e^x}{n!}\frac{\mathrm d^n}{\mathrm dx^n}(\mathrm e^{-x}x^n)}
assoc_laguerre(n,m,x) 广义拉盖尔多项式 \displaystyle{P_n^m(x)=\frac{(-1)^m}{2^nn!}(1-x^2)^{m/2}\frac{\mathrm d^{n+m}}{\mathrm dx^{n+m}}(x^2-1)^n}
legendre(n,x) 勒让德多项式 \displaystyle{P_n^m(x)=\frac{(-1)^m}{2^nn!}(1-x^2)^{m/2}\frac{\mathrm d^{n+m}}{\mathrm dx^{n+m}}(x^2-1)^n}
assoc_legendre(n,m,x) 广义勒让德多项式 \displaystyle{P_n^m(x)=\frac{(-1)^m}{2^nn!}(1-x^2)^{m/2}\frac{\mathrm d^{n+m}}{\mathrm dx^{n+m}}(x^2-1)^n}
sph_legenre(l,m,theta) 球勒让德多项式 \displaystyle{P_n^m(x)=\frac{(-1)^m}{2^nn!}(1-x^2)^{m/2}\frac{\mathrm d^{n+m}}{\mathrm dx^{n+m}}(x^2-1)^n}
cyl_bessel_j(alpha,x) 第一类贝塞尔函数 \displaystyle{J_\alpha(x)=\sum_{m=0}^\infty\frac{(-1)^m(x/2)^{2m+\alpha}}{m!\Gamma(m+\alpha+1)}}
cyl_bessel_i(alpha,x) 第一类修正贝塞尔函数 \displaystyle{I_\alpha(x)=\sum_{m=0}^\infty\frac{(x/2)^{2m+\alpha}}{m!\Gamma(m+\alpha+1)}}
cyl_neumman(alpha,x) 第二类贝塞尔函数(诺依曼函数) \displaystyle{Y_\alpha(x)=\frac{J_\alpha(x)\cos(\alpha\pi)-J_{-\alpha}(x)}{\sin{\alpha\pi}}}
cyl_bessel_k(alpha,x) 第二类修正贝塞尔函数 \displaystyle{K_\alpha(x)=\frac\pi2\frac{I_{-\alpha}(x)-I_\alpha(x)}{\sin(\alpha\pi)}}
sph_neumann(n,x) 球诺依曼函数 \displaystyle{y_n=\sqrt{\frac\pi{2x}}Y_{n+\frac12}(x)}

文件系统库<filesystem>

标准化系统对文件操作的接口。直接来自于boost::filesystem,很大程度上基于POSIX,不过其灵活性使其足以在各个系统移植。

并行化库<execution>

提供了三种标准执行策略作为<algorithm>库中一系列重载的新函数的参数(总计69种函数),规定其执行策略。

  • std::parallel::sequantial_execution_policy,串行执行策略,全局对象std::seq
  • std::parallel::parallel_execution_policy,并行执行策略,全局对象std::par
  • std::parallel::parallel_vector_execution_policy,并行向量执行策略,全局对象std::par_vec

因为暂时还没有编译器支持,所以我对这些东西知之甚少。需要学习的话需要翻标准了。

新算法

  • for_each_nfor_each_n版本
  • reduce,同accumulate,只是不再保证范围内顺序执行,可能被分组重排
  • transform_reduce,等价于先执行一次transform,再执行reduce
  • inclusive_scan,类似于前缀和的前缀运算,inclusive指运算结果中的第i项包含原数列第i项,即b[i] = f(a[i], b[i - 1])
  • exclusive_scan,类似于前缀和的前缀运算,exclusive指运算结果中的第i项不包含原数列的第i项,即b[i] = f(a[i - 1], b[i - 1])
  • transform_inclusive_scan,等价于先执行一次transform,再执行`inclusive_scan`
  • transform_exclusive_scan,等价于先执行一次transform,再执行`exclusive_scan`

std::basic_string_view

所谓的immutable类,等价于Java中的String。当然也有几个别名std::string_viewstd::basic_string_view<char>以及其它类似的std::wstring_viewstd::u16string_viewstd::u32string_view

std::any

前方高能!可以接受任何类型的类型,来自boost::any,用type_info存储类信息,在需要取出某个类型的值时候使用std::any_cast<T>()传入类型,如果类型不匹配会抛出一个std::bad_any_cast异常。

给出一个std::any的例子来证明一下有多强大。

#include <any>
#include <iostream>
#include <string>
#include <vector>

int main() {
    using namespace std::literals;
    std::vector<std::any> va = {
        2147483647,  // int
        3.1415926,  // double
        "string"s,  // std::string
        []() { },  // lambda expression
    };
}

是不是感觉和动态类型编程语言一样了?不过这种抽象当然有其对应的代价,由于需要保存对应的对象的信息,需要利用C++可怜的RTTI,还需要一定的运行时代价。

std::variant

高能不断!可以接受参数列表中的类型,来自boost::variant。在取值的时候需要使用std::visit提供一个可调用对象来匹配类型,和上文说的类似,可以使用编译期constexpr if来简化书写,一个简单的例子。

#include <iostream>
#include <variant>
#include <vector>

int main() {
    std::vector<std::variant<int, double>> vvid = {
        2147483647,  // int
        3.1415926,  // double
    };
    for (auto &vid: vvid) {
        std::visit([]<typename T>(T &&id) {
            using type = std::decay_t<T>;
            if constexpr(std::is_same_v<type, int>) {
                std::cout << "int value: " << id << '\n';
            } else if constexpr(std::is_same_v<type, double>) {
                std::cout << "double value: " << id << '\n';
            }
        }, vid);
    }
}

std::optional

已经有很多语言有了对Optional的语言级支持,就算没有也都在标准库中引入了Optional,实践证明Optional的可读性、可用性比基于异常的程序好用。C++17中的std::optional则来自于boost::optionalstd::optional<T>可以用std::variant<T, std::nullopt_t>实现,这个std::nullopt_t又是引入来支持std::optional的。

这是个比较广泛的概念,就不细述了。

新算法std::sample

和随机数有关的算法。从一个区间取样n个,使得每个元素的出现概率相同,结果输出到一个Output Iterator。

函数std::invoke

这是个用来调用函数的函数。std::invoke可以直接通过传入的参数调用函数,用std::invoke(fn, ...args)的形式调用任何std::is_invocable,比如std::function、lambda表达式、类的成员函数等等物件。

好了,最后一个问题std::invoke能不能invoke自己呢?

Trait std::is_invocable, std::is_invocable_rstd::invoke_result

顾名思义,判断一套参数类型是否可以调用,格式为std::is_invocable<Fn, ...Args>.

std::is_invocable_r<Ret, Fn, ...Args>则是判断返回类型能否被cast为Ret类型。

std::invoke_result在前面废弃std::result_of有过叙述,不再重复。

初等字符串转换

std::to_charsstd::from_chars,是将各种类型转化为char序列的安全版本。返回一个结构体,如果出错返回值中会设置std::errc

别名std::void_t

来自boost::hanavoid_t,简化模板元编程。本质就是一个template <typename ...Args> using void_t = void;Args中的表达式如果不合法则会导致SFINAE匹配不到。

std::enable_if稍微好看一点,不过还是很丑陋。

别名std::bool_constant

前有std::integral_constant,对其第一个参数的特化为bool就是std::bool_constant。个人猜测还是为了简化std::true_typestd::false_type的定义吧。

逻辑运算元函数

包含std::conjunctionstd::disjunctionstd::negation,分别对所有模板参数的value成员取与、或、非。

讲道理名字这么长,还不如用C++17的折叠表达式。

std::swap提供帮助的traits

包括std::is_swappablestd::is_swappable_withstd::is_nothrow_swappablestd::is_nothrow_swappable_with

前两个第一个是只接受一个模板参数,判断这个类自身能不能作为两个参数传入std::swap,后者接受两个模板参数,判断两个类能不能作为参数传入std::swap。后面两个嘛,顾名思义。

Trait std::is_aggregate

判读类型是否可以使用聚合初始化。大概也是为了C++20的服务的?

C99早就支持在大括号初始化中指名某个单独的元素初始化,这么多年来却不见C++有什么改观。大概是担心聚合初始化和构造函数相冲突。不过C++20有希望引入,拭目以待。

Trait std::has_unique_object_representations

名字长得要死。一句话解释就是类的实例里每个字节都表示了某个成员数据,而不是为了对齐的填充、或者储存虚指针等作用,另外还要求可平凡复制,也就是不能有自定的拷贝函数。似乎是为了std::hash服务的。

函数std::as_const

返回实体的常量引用。嗯……返回类型是给传入的参数类型里加了个const

非成员的std::sizestd::datastd::empty函数

std::begin std::end类似,提供了全局的这几个函数,主要是为了方便泛型编程吧,也就是可以给数组执行这些操作了。

函数std::clamp

钳子,对于传入参数std::clamp(x, high, low),如果lowxhigh则返回x,否则x ≤ low则返回lowhigh ≤ x则返回high

函数std::gcdstd::lcm

顾名思义,最大公约数与最小公倍数。提供标准支持,规范行为。

std::shared_mutex

读写锁的标准支持,满足标准布局和共享互斥,接口和普通的std::mutex一致,除此之外还有try_lock_sharedlock_sharedunlock_shared三个成员函数提供读共享写互斥,也可以用于std::lock_guard

常量std::hardware_constructive_interference_sizestd::hardware_destructive_interference_size

硬件相关的常量,和CPU Cache有关,为多线程准备的。不是很了解,摘录一下cppreference.com上的内容。

std::hardware_constructive_interference_size为鼓励真共享的最大连续内存大小。

std::hardware_destructive_interference_size为二个对象间避免假数据共享的最小偏移。

函数std::apply

std::invoke差不多,只是函数参数打包为一个类std::tuple对象作为第二个参数传入。

我在实现std::integer_sequence的时候配合std::invoke顺便实现过一遍,倒是不难。

函数std::make_from_tuple

std::apply类似,不过std::make_from_tuple<T>(Tuple &&)是用于构造类型T的实例的时候用的,返回从Tuple &&构造的T

函数std::not_fn

目的是取代在已废弃的std::not1std::not2,这两个都是针对一元、二元函数的取反包装器。调用std::not_fn返回类型简单地等价于对std::invoke相同参数进行取反的结果。

多态分配器std::memory_resourcestd::polymorphic_allocator

由于分配器类型是所有标准库容器类型的一部分,所以分配器上的不同会导致两个std::vector的类型完全不同,无法进行相互转换。std::pmr::polymorphic_allocator就是为了解决这个问题的,它的不同的实例会根据构造时使用的std::pmr::memory_resource的不同而有不同的表现,以达到多态的效果。

程序员则可以通过继承std::pmr::memory_resource并改写自己的分配器行为来实现不同的分配器,比如说构造std::pmr::vector的时候只需要传入自定义的继承自std::pmr::memory_resource的类的实例即可,而且这些std::pmr::vector的分配器均为std::pmr::polymorphic_allocator,就是说std::pmr::vector<T>其实都是std::vector<T, std::pmr::polymorphic_allocator>,所以所有的实例都是一个类型,可以进行相互赋值等操作。

除了std::pmr::vector,在std::pmr命名空间下还有一系列的利用了std::pmr::polymorphic_allocator作为分配器的容器别名。

Boyer Moore搜索算法标准化

std::search提供了新的重载,除了并行策略之外,还提供了std::default_searcherstd::boyer_moore_searcherstd::boyer_moore_horspool_searcher三种算法的重载。

这两者的时间复杂度和KMP相同,均为O(m+n),但是最差空间复杂度达到了O(mn),好在后者的平均空间复杂度还是有O(m+n)的。

使用方法是通过两个Input Iterator构造出一个Searcher,然后Searcher作为一个单独的参数传入std::search中,举例而言

#include <algorithm>
#include <functional>
#include <iostream>
#include <string_view>

int main() {
    using namespace std::literals;
    std::string_view haystack = "Valentine's Day"sv;
    std::string_view needle = "lent"sv;
    std::boyer_moore_horspool_searcher bmhs{needle.begin(), needle.end()};
    std::cout << std::search(haystack.begin(), haystack.end(), bmhs)
              << std::endl;
}

对已存在特性的修改

无错误讯息的static_assert

允许static_assert省略第二个参数。

嵌套命名空间定义

允许使用namespace foo::bar { }来定义namespace foo { namespace bar { } }这种嵌套命名空间。

模板模板形参中的typename

template <template <param> class T>中的class从C++17开始允许使用typename替换。这样在模板中typenameclass完全没有区别了。

typenameclass的区别就仅仅在模板外有区别,比如说声明定义一个类的时候只能用class,而指明一个作用域里的名称是一个类型就要加上typename

基于范围的for循环分离定义beginend

C++17之前的for (range_decl: range_expr) { loop_stmts }的定义是

{
    auto && __range = range_expr;
    auto __begin = std::begin(__range),
         __end = std::end(__range);
    for ( ; __begin != __end; ++__begin) {
        range_decl = *__begin;
        loop_stmts
    }
}

这里就要求std::begin(__range)std::end(__range)类型相同。C++17更新成了

{
    auto && __range = range_expr;
    auto __begin = std::begin(__range);
    auto __end = std::end(__range);
    for ( ; __begin != __end; ++__begin) {
        range_decl = *__begin;
        loop_stmts
    }
}

这样就只要求std::begin(__range)std::end(__range)可进行不等于比较,而不要求这两者类型相同。再顺便一提,C++17引入的带初始化ifswitch在C++20会被添加到基于范围的for

using声明使用逗号分隔多个实体

主要是对命名空间中不同实体必须每个使用一个 using 来引入, C++17 才允许直接在一个 using 中引入多个,使用逗号分隔。

这个更改的主要用途是 1. 语法糖,2.允许在类里using模板参数的时候展开参数包。

说实话这个在 C++17 才引入让我十分震惊,这是个十分好用的语法,可变模板参数 C++11 引入,这个特性最迟也不能过 C++14 吧?

比如在调用std::visit的时候,采用overlords形式构造一个可调用的类的时候需要using父类们的operator(),在C++17中可以这样写

#include <iostream>
#include <variant>
#include <vector>

template<typename ...Ts>
struct overloaded : Ts... {
    // Pack expansion in using-decl
    using Ts::operator()...;
};

// User-defined deduction guide
template<typename ...Ts>
overloaded(Ts...) -> overloaded<Ts...>;

int main() {
    std::vector<std::variant<int, double>> vvid{2147483647, 3.1415926};
    for (auto &&vid: vvid) {
        std::visit(overloaded {
            [](int i) {
                std::cout << "int value: " << i << '\n';
            },
            [](double d) {
                std::cout << "double value: " << d << '\n';
            },
        }, vid);
    }
}

枚举的直接列表初始化

比如Enum e在C++17之前必须使用Enum e{Enum(5)}来构造,C++17开始可以直接Enum e{5}构造。

基于列表初始化的新auto规则

不再允许auto l{1, 2}这种语法,必须要经过一次拷贝,也就是auto l = {1, 2},构造出来的才是一个std::initializer_list<int>,否则是不合法的。

个人猜测是因为auto l{1, 2}的语法歧义太大,几乎所有接受两个int参数的构造,所有接受std::initializer_list<int>作为参数的构造都可以通过。

函数std::uncaught_exceptions

原来的std::uncaught_exception已经在C++17被弃用,建议使用这个替代。这个函数返回当前未被捕获的异常个数,而std::uncaught_exception返回存不存在未被捕获的异常。提案里提到原先的函数用处不是很大,还是提供数量可能有作用。

命名空间和枚举的属性

比如说这就可以直接对一整个命名空间进行[[deprecated]]等操作。

std::mapstd::unordered_map增加成员函数try_emplace

try_emplace接受第一个参数为键值,如果已经在映射表里存在了则不进行任何操作,包括右值的移动。如果不存在则emplace这个参数。

std::mapstd::set增加切片操作

std::mapstd::set毕竟还是用红黑树实现的多,完全可以支持切片、合并等操作,于是终于在新标准中加入了这一操作,可以对set::mapstd::set进行区间转移。

std::mapstd::set提供了extract用来转移单个节点和merge操作以合并两个映射或集合。

string::data返回类型中移除const

因为string::data的返回值在提供给一些函数作为参数时可能没有const声明,需要复制一份,增加了麻烦。

文档中的例子是给Windows的一个API传参的时候需要变回字符数组,但是由于string::data带有const修饰,需要复制一份出来。

std::scoped_lock

std::lock_guard的区别是std::scoped_lock接受多个模板参数,可以一次性为多个锁上锁。规范是从左往右上锁,如果其中有一个锁获取失败则算上锁失败,所有的已经上锁的锁都会被逐个解锁。

为所有带::value成员的traits提供_v别名

字面意思,对于一系列的type traits提供对应的_v别名。

std::atomic增加静态成员is_always_lock_free

标准允许std::atomic使用互斥锁实现,增加静态成员变量is_always_lock_free来判断对于某个类型的所有操作是否均可免锁。另一个成员函数is_lock_free则是动态检查是否使用了锁。

资料里查来说之所以一开始没有使用静态成员变量而使用动态查询的原因是考虑到平台可能会升级对某些类型的原子操作。但是感觉解释含糊不清,所以最后还是提供了一个静态的常量吗。

std::shared_ptr的数组支持

不是说原来不支持数组,只是新标准为std::shared_ptr<T[]>提供了默认的、正确的deleter,否则需要用户自己提供delete [] p的deleter才能正常回收内存,不然就会造成未定义行为。

std::shared_ptr新增成员类型weak_type

就是在类内增加了一句typedef weak_ptr<T> weak_type

std::hypot增加三参数重载

原先的std::hypot(x, y)返回\sqrt{x^2+y^2},增加重载std::hypot(x, y, z)返回\sqrt{x^2+y^2+z^2}

支持未初始化区域操作的函数

包括

  • std::unintialized_value_construct,接受两个迭代器,或者可以有执行策略,在迭代器范围内根据迭代器的value_type进行值初始化
  • std::unintialized_default_construct,接受两个迭代器,或者可以有执行策略,在迭代器范围内根据迭代器的value_type进行默认初始化
  • std::unintialized_copy,接受三个迭代器,或者可以有执行策略,将前两个迭代器之间的内容拷贝构造到第三个迭代器之后的空间。
  • std::unintialized_move,接受三个迭代器,或者可以有执行策略,将前两个迭代器之间的内容移动构造到第三个迭代器之后的空间。
  • std::unintialized_fill,接受两个迭代器和一个值,或者可以有执行策略,在迭代器范围内根据迭代器的value_type使用第三个参数进行构造初始化。
  • std::destroy,接受两个迭代器,或者可以有执行策略,在迭代器范围内进行析构。
  • std::destroy_at,接受一个指针,析构。

除了以上的,还有相应的_n版本的算法。

降低对分配器的要求

分配器可以接受部分的不完整类型,只要求满足分配器完整性要求,这又是个C++17的新概念——

AllocT的分配器时,当

  • Alloc为完整类型
  • 除了value_type,所有std::allocator_traits<Alloc>成员均为完整类型

时满足分配器完整性要求。

这一点最大的用处就是可以声明struct X { std::vector<X> vx; };这种类型。

针对<chrono>头文件的修改

增加了几个函数的重载,包括std::floorstd::ceilstd::roundstd::abs以实现对时间之间相互转换的取整,对于大部分函数增加了constexpr声明。

针对char_traits的修改

这项提案主要是为std::basic_string_view服务的。要求增加几个函数的constexpr声明。

改进了std::pairstd::tuple

视条件增加了std::pairstd::tuple一系列构造函数的explicit声明。条件太多,不列出来了……

std::common_type的更新

涉及到细节有点繁琐了。总之在std::common_type的定义上又增添了几句话。

杂项

C++标准中的C指代C11

增加所有的stdX为保留命名空间

提供C头文件提要

增加几个术语

  • forwarding reference
  • default member initializer
  • template entity
  • contiguous iterator

修改术语随机数生成器为随机比特生成器

关于C++20

虽然C++20还没有正式决定就叫做C++20,暂时还是C++2a,不过还是希望能在2020年见到标准的。

暂时来说C++20确定的东西有

  • P0734R0 Concept TS
  • P0683R1 位域的默认成员初始化器 P0683R1
  • P0704R1 const& 限定的指向成员指针
  • P0409R2 允许lambda捕获[=, this]
  • P0306R4 宏__VA_OPT__
  • P0329R4 指代初始化器
  • P0428R2 泛型lambda的模板形参列表
  • P0702R1 类模板实参推导中的 initializer_list 构造函数
  • P0614R1 基于范围的for的初始化语句
  • P0515R0 三路比较运算符<=>
  • P0515R3 三路比较运算符支持头文件<compare>
  • P0463R1 std::endian
  • P0674R1 扩展std::make_shared以支持数组
  • P0020R6 浮点原子类型
  • P0053R7 同步的有缓冲输出流
  • P0202R3 <algorithm><utility>constexpr
  • P0415R1 <complex>更多的constexpr
  • P0550R2 std::remove_cvref
  • P0457R2 字符串的starts_withends_with
  • P0653R2 将指针转换到裸指针的工具

提一提C++2a几个比较有名的新玩意儿,有<=>运算符,所谓的 Three-way comparison operator,或者叫飞船(Capsule)运算符,可以被重载。还有一个是老早就被提案,至今没有落实的concept constraints,主要作用大概是有效地优化编译错误信息,简化模板元里对参数增加限制。然而concepts一开始还有一个axiom关键字,是用来约束返回值的(相应的,concept是用来约束返回类型、参数成员的),但是因为无法有效地在编译期实现检查,最终被抛弃了。

另外有希望进入C++20的有网络编程的STL支持(讲真,C++大概真的是第二个没有标准网络库的语言了,C是第一个)Network TS、Ranges TS对容器范围操作的抽象,一个Module TS,增强C++的模块化资辞,还有比较新的一个提案——编译期反射,还有一个Lambda表达式可以写模板参数,从此一个Lambda可以集齐[]<>(){}四套括号了。就拭目以待吧。

发表评论