C++11 重读笔记

2022/1/21

# 稳定性与兼容性

#

  1. __func__ 预定义标识符,用于返回所在函数的名字,c++11 中可
  2. 预定义宏
    1. __STDC_HOSTED__ 编译器目标系统是否包含完整的标准 C 库
    2. __STDC_VERSION__ 支持的 C 标准版本
    3. __STDC_IOS_10646__ 通常定义为 yyyymmL 格式的整数常量,例如 199712L,用来表示 C++ 编译环境符合某个版本的 ISO/IEC 10646 标准
  3. _Pragma 操作符,与预处理指令 #pragma 功能相同的操作符 _Pragma
  4. __cplusplus ,通常被定义为一个整型值,c++ 03 标准中为 199711L,c++ 11 标准中为 201103L

# 拓展的整型

编译器可以定义拓展整型。(当考虑移植性的时候,应该如何思考变量类型,是一个值得思考的问题

# 静态断言

希望编译期就能做一些断言,可用 static_assert

# noexcept 修饰符

表明该函数不抛出异常,如抛出,则 noexcept 修饰的函数科恩那个通过 std::terminate 的调用来“暴力”结束程序的执行

# final/override 控制

针对虚函数的重载,final 提供了最后一层接口,不再允许派生类重载该函数,而 override 则是必须在派生类中重载基类虚函数

# 通用性与专用性

# 继承构造函数

通过 using A::A 的声明,把基类中的构造函数悉数继承到派生类中。同时 c++ 11 将标准继承构造函数设计为跟派生类中的各种类默认函数一样,是隐式声明的。

# 委派构造函数

委派函数不能同时“委派”和使用初始化列表

# 右值移动

# 拷贝构造函数

就是避免悬空指针

# 移动语句

移动内存所有权:例如将临时的变量的指针置为空,保存变量的指针指向数据

# 左值、右值

在 C++11 中,右值是由两个概念构成的,一个是将亡值( xvalue, eXpiring Value),另一个则是纯右值 ( prvalue, Pure Rvalue )。

  • 其中纯右值就是 C++98 标准中右值的概念,讲的是用于辨识临时变量和一些不跟对象关 联的值。比如非引用返回的函数返回的临时变量值就是一个纯右值。一些运算表达式,比如 1 + 3 产生的临时变量值,也是纯右值。而不跟对象关联的字面量值,比如: 2、’c’、 true,也是纯右值。此外,类型转换函数的返回值、lambda 表达式等,也都是右值。

  • 而将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用 T&& 的函数返回值、std:move 的返回值,或者转换为 T&& 的类型转换函数的返回值。而剩余的,可以标识函数、对象的值都属于左值。

在 C++11 的程序中,所有的值必属于左值、将亡值、纯右值三者之一。

# std::move

std:move 的作用是强制一个左值成为右值,一是用来配合移动语义,在生命周期即将结束之前转化为右值配合移动语句;二是用于转发。

# 完美转发与引用折叠

对于转发函数,形参和实参带有左值引用,优先左值引用,随后才是右值引用。

# 列表初始化(防止类型收窄)

包括 <initializer_list> 头文件即可

# POD

POD 好处:

  1. 字节赋值,代码中我们可以安全地使用 memsetmemcpy 对 POD 类型进行初始化和拷贝等操作。
  2. 提供对 C 内存布局兼容。C++ 程序可以与 C 函数进行相互操作,因为 POD 类型的数据在 C 与 C++ 间的操作总是安全的。
  3. 保证了静态初始化的安全有效。静态初始化在很多时候能够提高程序的性能,而 POD 类型的对象初始化往往更加简单(比如放入目标文件的 .bss 段,在初始化中直接被赋 0)。

TODO:空了回头再过来看看

# 联合体

不想了解,心智负担

# 用户自定义字面量

通过 classname operator "" _N(T x) {} 可定义,当然也有一些限制:

  1. 如果字面量为整型数,那么字面量操作符函数只可接受 unsigned long long 或者 const char* 为其参数。当 unsigned long long 无法容纳该字面量的时候,编译器会自动将该字面量转化为以 \0 为结束符的字符串,并调用以 const char* 为参数的版本进行处理。
  2. 如果字面量为浮点型数,则字面量操作符函数只可接受 long double 或者 const char* 为参数。const char* 版本的调用规则同整型的一样(过长则使用 const char* 版本)。
  3. 如果字面量为字符串,则字面量操作符函数函数只可接受 const char*size_t 为参数(已知长度的字符串)。
  4. 如果字面量为字符,则字面量操作符函数只可接受一个 char 为参数。

NOTE: 如果重用后缀 “L”,则可能引起混乱,比如 201203L 这样的宏。

# 内联名字空间

using namespace name / inline

# 模板别名

usingtypedef 更灵活

# SFINAE

# 易用性

# auto

YYDS,auto 无法带走(取消) constvolatile 属性

# decltype

# typeid & decltype

与 C 完全不支持动态类型不同的是,C++ 在 C++98 标准中就部分支持动态类型了。C++98 对动态类型支持就是 C++ 中的运行时类型识别(RTTI)。

RTTI 的机制是为每个类型产生一个 type_info 类型的数据,可以在程序中使用 typeid 随时查询一个变量的类型,typeid 就会返回变量相应的 type_info 数据。而 type_info 的 name 成员函数可以返回类型的名字。而在 C++11 中,又增加了 hash_code 这个成员函数,返回该类型唯一的哈希值,以对变量的类型随时进行比较。

除了 typeid 外,RTTI 还包括了 C++ 中的 dynamic_cast 等特性。不过不得不提的是,由于 RTTI 会带来一些运行时的开销,所以一些编译器会让用户选择性地关闭该特性(比如 XL C/C++ 编译器的 -qnortti, GCC 的选项 -fno-ttion,或者微软编译器选项 /GR- )。而且很多时候,运行时才确定出类型为时过晚,更多需要的是在编译时期确定出类型(标准库中非常常见)。而通常是要使用这样的类型而不是识别该类型,因此 RTTI 无法满足需求。

C++11 中 decltype 推导返回类型的规则将依序判断以下四规则:

  1. 如果是一个没有带括号的标记符表达式(id-expression)或者类成员访问表达式,那么 decltype() 就是所命名的实体的类型。此外,如果是一个被重载的函数,则会导致编译时错误。

  2. 否则,假设类型是T,如果是一个将亡值(xvalue),那么 decltype()T&&

  3. 否则,假设类型是T,如果是一个左值,则 decltype()T&

  4. 否则,假设类型是T,则 decltype()T

这里我们要解释一下标记符表达式( id-expression)。基本上,所有除去关键字、字面量等编译器需要使用的标记之外的程序员自定义的标记(token)都可以是标记符(identifier)。而单个标记符对应的表达式就是标记符表达式。比如程序员定义了: int arr [4];那么 arr 是一个标记符表达式,而 arr[3] + 0arr[3] 等,则都不是标记符表达式。

# cv 限制符的继承与冗余的符号

与 auto 类型推导时不能“带走” cv 限制符不同,decltype 是能够“带走”表达式的 cv 限制符的。不过,如果对象的定义中有 const 或 volatile 限制符,使用 decltype 进行推导时,其成员不会继承 const 或 volatile 限制符。

# 追踪返回类型

template<typename T1, typename T2 >
auto Sum(T1 & t1, T2 & t2) -> decltype(t1 + t2) {
	return t1 + t2 ;
}
1
2
3
4

# 基于范围的 for 循环

for (auto e: a) {
    ...
}
1
2
3

# 提高类型安全

# 堆内存管理

  1. 野指针:一些内存单元已被释放,之前指向它的指针却还在被使用。这些内存有可能被运行时系统重新分配给程序使用,从而导致了无法预测的错误。
  2. 重复释放:程序试图去释放已经被释放过的内存单元,或者释放已经被重新分配过的内存单元,就会导致重复释放错误。通常重复释放内存会导致C/C++运行时系统打印出大量错误及诊断信息。
  3. 内存泄漏:不再需要使用的内存单元如果没有被释放就会导致内存泄漏。如果程序不断地重复进行这类操作,将会导致内存占用剧增。

# 智能指针

  1. std::shared_ptr
  2. std::unique_ptr
  3. std::weak_ptr

# 垃圾回收

  1. 基于引用计数( reference counting garbage collector )的垃圾回收器

    引用计数也不会对系统的缓存或者交换空间造成冲击,因此被认为“副作用”较小。但是这种方法比较难处理“环形引用”问题,此外由于计数带来的额外开销也并不小,所以在实用上也有一定的限制。

  2. 基于跟踪处理( tracing garbage collector)的垃圾回收器

    1. 标记-清除( Mark-Sweep )
    2. 标记-整理( Mark-Compact )
    3. 标记-拷贝(Mark-Copy)

# 高性能

# 常量

const – 运行时常量

constexpr – 编译期常量

# 常量表达式

  1. 函数体只有单一的return返回语句。
  2. 函数必须返回值(不能是void函数)。
  3. 在使用前必须已有定义。
  4. return 返回语句表达式中不能使用非常量表达式的函数、全局数据,且必须是一个常 量表达式。

# 常量表达式的构造函数

使用上的约束,主要的有以下两点:

  1. 函数体必须为空。
  2. 初始化列表只能由常量表达式来赋值。

# 变长模板

...
1

# 原子类型与原子操作

# 内存模型,顺序一致性与 memory_order

多线程的程序总是共享代码的,强顺序意味着:对于多个线程而言,其看到的指令执行顺序是一致的。

对于共享内存的处理器而言,需要看到内存中的数据被改变的顺序与机器指令中的一致。反之,如果线程间看到的内存数据被改变的顺序与机器指令中声明的不一致的话,则是弱顺序的。

大多数 atomic 原子操作都可以使用 memory_order 作为个参数

# 线程局部存储

线程局部存储(TLS, thread local storage)是一个已有 的概念。简单地说,所谓线程局部存储变量,就是拥有线程生命期及线程可见性的变量。

线程局部存储实际上是由单线程程序中的全局/静态变量被应用到多线程程序中被线程共享而来。我们可以简单地回顾下所谓的线程模型。通常情况下,线程会拥有自己的栈空间,但是堆空间、静态数据区( 如果从可执行文件的角度来看,静态数据区对应的是可执行文件的 data、bss 段的数据,而从 C/C++ 语言层面而言,则对应的是全局/静态变量)则是共享的。这样一来,全局、静态变量在这种多线程模型下就总是在线程间共享的。

C++11 对 TLS 标准做出了一些统一的规定。声明一个 TLS 变量的语法很简单,即通过 thread_ local 修饰符声明变量即可。

一旦声明一个变量为 thread_local,其值将在线程开始时被初始化,而在线程结束时,该值也将不再有效。对于 thread_local 变量地址取值(&),也只可以获得当前线程中的 TLS 变量的地址值。

# 快速退出

...
1

在 C++ 程序中,一些有关“终止”的函数,如 terminateabortexit等。这些函数容易让人产生疑惑,因为对于普通的程序来说,它们都只是终止程序的运行而已。不过实际上它们还是有很大的区别的。因为其对应的是“正常退出”和“异常退出”两种情况。

首先我们可以看看 terminate 函数,terminate 函数实际上是 C++ 语言中异常处理的一部分(包含在 头文件里)。一般而言,没有被捕捉的异常就会导致 terminate 函数的调用。此外我们在第2章中提到过的 noexcept 关键字声明的函数,如果抛出了异常,也会调用 terminate 函数。其他还有很多的情况。但直观地讲,只要 C++ 程序中出现了非程序员预期的行为,都有可能导致 terminate 的调用。而 terminate 函数在默认情况下,是去调用 abort 函数的。不过用户可以通过 set _terminate 函数来改变默认的行为。因此,可以认为在 C++ 程序的层面,termiante 就是“终止”。

相对于 termiante,源自于C中(头文件 )的 abort 则更加低层。abort 函数不会调用任何的析构函数(读者也许想到了,默认的 terminate 也是如此),默认情况下,它会向合乎 POSIX 标准的系统抛出一个信号(signal): SIGABRT。如果程序员为信号设定一个信号处理程序的话(signal handler),那么操作系统将默认地释放进程所有的资源,从而终止程 序。可以说,abort 是系统在毫无办法下的下下策一终止进程。有时候这会带来一些问题。典型的,倘若被终止的应用程序进程与其他应用程序软件层有一些交互(比如一些硬件驱动程序,一些通过网络通信的程序等,假设这些程序设计得并不那么健壮),那么本进程的意外终止,都可能导致这些交互进程处于一些“中间状态”,进而出现一些问题。

相比而言,exit 这样的属于“正常退出”范畴的程序终止,则不太可能有以上的问题。exit 函数会正常调用自动变量的析构函数,并且还会调用 atexit 注册的函数。这跟 main 函数结束时的清理工作是一样的。

不过有的时候,main 或者使用 exit 函数调用结束程序的方式也不那么令人满意。有的时候,代码中会有很多的类,这些类在堆空间上分配了大量的零散的内存(直接从堆里分配,并没有优化的策略),而 main 或者 exit 函数调用会导致类的析构函数依次将这些零散的内存还给操作系统。这是一件费时的工作,而实际上,这些堆内存将在进程结束时由操作系统统 一回收(事实上这相当快, 操作系统除了释放一 些进程相关的数据结构外,只是将- -些物理内存标记为未使用就可以了)。如果这些堆内存对其他程序不产生任何影响,那么在程序结束时释放堆内存的析构过程往往是毫无意义的。因此,在这种情况下,我们常常需要能够更快地退出程序。

另外,在多线程情况下,我们要使用 exit 函数来退出程序的话,通常需要向线程发出一个信号,并等待线程结束后再执行析构函数、atexit 注册的函数等。这从语法上讲非常正确,不过这样的退出方式有的时候并不能够像预期那样工作,比如说线程中的程序在等待 I/O 运行结束等。在一些更为复杂的情况下,可能还会遭遇到一些因为信号顺序而导致的死锁状 况。一旦出现了这样的问题,程序往往就会被“卡死”而无法退出。

为此,在 C++11 中,标准引入了 quick_ exit 函数,该函数并不执行析构函数而只是使程序终止。与 abort 不同的是,abort 的结果通常是异常退出(可能系统还会进行 coredump 等以辅助程序员进行问题分析),而 quick_ exit 与 exit 同属于正常退出。此外,使用 at_ quick exit 注册的函数也可以在 quick_exit 的时候被调用。这样一来,我们同样可以像 exit 一样做一些清理的工作(这与很多平台,上使用 _exit 函数直接正常退出还是有不同的)。在 C++11 标准中,at__quick_exit 和 at_exit 一样,标准要求编译器至少支持 32 个注册函数的调用。

# 新事物

# 空指针

nullptr
1
  1. 所有定义为 nullptr_t 类型的数据都是等价的,行为也是完全一致。
  2. nullptr_t 类型数据可以隐式转换成任意一个指针类型。
  3. nullptr_t 类型数据不能转换为非指针类型,即使使用 reinterpret_cast<nullptr_t>) 的方 式也是不可以的。
  4. nullptr_t 类型数据不适用于算术运算表达式。
  5. nullptr_t 类型数据可以用于关系运算表达式,但仅能与 nullptr_t 类型数据或者指针类 型数据进行比较,当且仅当关系运算符为 ==<=>= 等时返回 true

# 默认函数控制

在 C++ 中声明自定义的类,编译器会默认帮助程序员生成一些他们未自定义的成员函数。这样的函数版本被称为“默认函数”。这包括了以下一些自定义类型的成员函数:

  1. 构造函数
  2. 拷贝构造函数
  3. 拷贝赋值函数(operator=)
  4. 移动构造函数
  5. 移动拷贝函数
  6. 析构函数

此外,C++ 编译器还会为以下这些自定义类型提供全局默认操作符函数:

  • operator,
  • operator &
  • operator &&
  • operator *
  • operator ->
  • operator ->*
  • operator new
  • operator delete

在 C++ 语言规则中,一旦程序员实现了这些函数的自定义版本,则编译器不会再为该类自动生成默认版本。有时这样的规则会被程序员忘记,最常见的是声明了带参数的构造版本,则必须声明不带参数的版本以完成无参的变量初始化。不过通过编译器的提示,这样的问题通常会得到更正。但更为严重的问题是,一旦声明了自定义版本的构造函数,则有可能导致我们定义的类型不再是 POD 的。

# "= default" 与 "= deleted"

= default 可以保持 POD

# lambda 函数

[capture] (parameters) mutable ->return- type {statement}

其中,

  • [capture]:捕捉列表。捕捉列表总是出现在 lambda 函数的开始处。事实上,是lambda引出符。编译器根据该引出符判断接下来的代码是否是 lambda 函数。捕捉列表能够捕捉上下文中的变量以供 lambda 函数使用。
  • (parameters):参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号 ()一起省略。
  • mutable:mutable 修饰符。默认情况下,lambda 函数总是一个 const 函数,mutable 可以取消其常量性。在使用该修饰符时,参数列表不可省略( 即使参数为空)。
  • -> return-type:返回类型。用追踪返回类型形式声明函数的返回类型。出于方便,不需要返回值的时候也可以连同符号一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编泽器对返回类型进行推导。
  • {statement}:函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。

C++11 中,默认情况下 lambda 函数是一个 const 函数。按照规则,一个 const 的成员函数是不能在函数体中改变非静态成员变量的值的。

# 应用

# 数据对齐

alignas()
align()
aligned_storage()
aligned_union()
1
2
3
4

注意在 C++11 标准之前,我们也可以使用一些编译器的扩展来描述对齐方式,比如 GNU 格式的 __attribute__(C __aligned__(8)) 就是一个广泛被接受的版本。

在 C++11 标准中规定了一个“基本对齐值" ( fundamental alignment)。一般情况下其值通常等于平台上支持的最大标量类型数据的对齐值(常常是 long double)。我们可以通过 alignof(std::max_align_t) 来查询其值。

# 通用

随着 C++ 语言的演化和编译器的发展,人们常会发现标准提供的语言能力不能完全满足要求。于是编译器厂商或组织为了满足编译器客户的需求,设计出了一系列的语言扩展(language extension)来扩展语法。这些扩展语法并不存在于 C++/C 标准中,却有可能拥有较多的用户。有的时候,新的标准也会将广泛使用的语言扩展纳人其中。扩展语法中比较常见的就是“属性”(attribute)。属性是对语言中的实体对象(比如函数、变量、类型等)附加一些的额外注解信息,其用来实现一些语言及非语言层面的功能,或是实现优化代码等的一种手段。不同编译器有不同的属性语法。比如对于 g++,属性是通过 GNU 的关键字 __attribute__ 来声明的。程序员只需要简单地声明 attribute( (attribute-list) ) 即可为程序中的函数、变量和类型设定一些额外信息,以便编译器可以进行错误检查和性能优化等。

noreturn
carries_dependency
1
2

# unicode 支持

直接 ICU (opens new window)

# 原生字面量

Last Updated: 2023-10-29T08:26:04.000Z