Skip to content

书引:

C++进阶图书

涵盖了C++11~C++20全部新增的语言核心特性

第一部分(第1~34章)是讲解基础特性

嘿,我有一个不错的新特性,可以解决你手中的问题,它的原理是……而且关于这个特性我还有一个小故事,想听听么?

第1章 新基础类型 C++11 ~ C++20

  • long long

  • char16_t char32_t

  • char8_t

long long

C99 开始支持

C++11 开始支持

long 通常表示一个 32 位整型,long long 则用来表示 64 位整型。C++标准中定义,long long 是一个至少为 64 位的整数类型。

  • long long 有符号类型

  • unsigned long long 无符号类型

最大值、最小值

  • LLONG_MAXLLONG_MINULLONG_MAX

    cpp
    #define LLONG_MAX 9223372036854775807LL         // long long的最大值
    #define LLONG_MIN (-9223372036854775807LL - 1)  // long long的最小值
    #define ULLONG_MAX 0xffffffffffffffffULL        // unsigned long long的最大值
  • numeric_ limits 类模板

    cpp
    std::numeric_limits<long long>::max()
    std::numeric_limits<long long>::min()
    std::numeric_limits<unsigned long long>::max()

char16_t与char32_t

C++11 开始支持

char16_t 对应 Unicode UTF-16

char32_t 对应 Unicode UTF-32

UTF-8、UTF-16、UTF-32 字符串对应的字面量表示:

  • UTF-8 => u8

  • UTF-16 => u 小写u

  • UTF-32 => U 大写U

wchar_t 在 Windows 上大小 2 个字节,在Linux/Unix 上大小 4 个字节,无法在不同操作系统平台保持一致。

char8_t

C++20 开始支持

char8_t具有和unsigned char相同的符号属性、存储大小、对齐方式以及整数转换等级。

cpp
char str[] = u8"text";  // C++17编译成功;C++20编译失败,需要char8_t
char c = u8'c';

第3章 auto 占位符 C++11 ~ C++20

  • auto

C++11标准赋予了auto新的含义:声明变量时根据初始化表达式自动推断该变量的类型、声明函数时函数返回值的占位符

第4章 decltype 说明符 C++11 ~ C++17

  • decltype C++11引入支持

typeid

typeid的返回值是一个左值,且其生命周期一直被扩展到程序生命周期结束。

typeid返回的std::type_info删除了复制构造函数,若想保存std::type_info,只能获取其引用或者指针:

cpp
auto t1 = typeid(int);     // 编译失败,没有复制构造函数无法编译
auto &t2 = typeid(int);    // 编译成功,t2推导为const std::type_info&
auto t3 = &typeid(int);    // 编译成功,t3推导为const std::type_info*

函数模板返回值类型推导应用:

cpp
template<class T1, class T2>
auto sum(T1 a1, T2 a2)->decltype(a1 + a2)
{
  return a1 + a2;
}

auto x4 = sum(5, 10.5);

在C++14标准中出现了decltype和auto两个关键字的结合体:decltype(auto)。

第6章右值引用 C++11 ~ C++20

左值、右值、将亡值

左值引用、右值引用

移动语义、完美转发

万能引用、引用折叠

cpp
#include <iostream>
#include <string>

template<class T>
void show_type(T t)
{
  std::cout << typeid(t).name() << std::endl;
}

template<class T>
void perfect_forwarding(T &&t)
{
  show_type(static_cast<T&&>(t));
}

优雅的完美转发:

cpp
template<class T>
void perfect_forwarding(T &&t)
{
  show_type(std::forward<T>(t));
}

第7章 lambda表达式 C++11 ~ C++20

  • lambda C++11

  • 广义捕获 C++11

  • 初始化捕获 C++14

  • 泛型lambda表达式 C++14

  • 常量lambda表达式 C++17

  • 捕获*this C++17

  • 无状态lambda表达式类型的构造与赋值 C++20

语法:

cpp
[ captures ] ( params ) specifiers exception -> ret { body }

C++14标准中定义了广义捕获,所谓广义捕获实际上是两种捕获方式,第一种称为简单捕获,这种捕获就是我们在前文中提到的捕获方法,即[identifier]、[&identifier]以及[this]等。

第8章 非静态数据成员默认初始化 C++11 ~ C++20

  • 非静态数据成员默认初始化 C++11

  • 数据成员的位域默认初始化 C++20

第9章 列表初始化 C++ C++20

  • 列表初始化 initialize list C++11

  • 指定初始化 C++20

使用括号初始化的方式叫作直接初始化,而使用等号初始化的方式叫作拷贝初始化(复制初始化)

cpp
#include <iostream>
#include <string>

struct C {
  C(std::initializer_list<std::string> a)
  {
       for (const std::string* item = a.begin(); item != a.end(); ++item) {
            std::cout << *item << " ";
       }
       std::cout << std::endl;
  }
};

int main()
{
  C c{ "hello", "c++", "world" };
}

第10章 默认和删除函数 C++11

  • =delete C++11

  • =default C++11

第11章 非受限联合类型 C++11

  • 非受限联合体类型 C++11
cpp
#include <iostream>
#include <string>
#include <vector>

union U
{
  U() : x3() {}
  ~U() { x3.~basic_string(); }
  int x1;
  float x2;
  std::string x3;
  std::vector<int> x4;
};

int main()
{
  U u;
  u.x3 = "hello world";
  std::cout << u.x3;
}

第12 ~ 16章

  • 委托构造函数 C++11
  • 继承构造函数 C++11
  • 强枚举类型 strong-typed enum C++11
  • override final C++11

override、overload、overwrite

第17章 基于范围的for循环 C++11 C++17 C++20

  • 基于范围的for循环 range-based for C++11
cpp
for ( range_declaration : range_expression ) loop_statement

第18章 支持初始化语句的if和switch C++17

  • 支持初始化语句的if和switch C++17

在C++17标准中,if控制结构可以在执行条件语句之前先执行一个初始化语句。

cpp
// if (init; condition) {}
#include <iostream>
bool foo()
{
  return true;
}
int main()
{
  if (bool b = foo(); b) {
       std::cout << std::boolalpha << "good! foo()=" << b << std::endl;
  }
}

第19章 static_assert 声明

  • static_assert

C++11 之前的替代方案:

cpp
#define STATIC_ASSERT_CONCAT_IMP(x, y) x ## y
#define STATIC_ASSERT_CONCAT(x, y) \
    STATIC_ASSERT_CONCAT_IMP(x, y)

// 方案1
#define STATIC_ASSERT(expr)                 \
    do {                                    \
        char STATIC_ASSERT_CONCAT(          \
            static_assert_var, __COUNTER__) \
            [(expr) != 0 ? 1 : -1];         \
    } while (0)

template<bool>
struct static_assert_st;
template<>
struct static_assert_st<true> {};

// 方案2
#define STATIC_ASSERT2(expr)    \
    static_assert_st<(expr) != 0>()

// 方案3
#define STATIC_ASSERT3(expr)        \
    static_assert_st<(expr) != 0>   \
    STATIC_ASSERT_CONCAT(           \
    static_assert_var, __COUNTER__)

利用的技巧是数组的大小不能为负值,当expr表达式返回结果为false的时候,条件表达式求值为−1,这样就导致数组大小为−1,自然就会引发编译失败。

static_assert声明是C++11标准引入的特性,用于在程序编译阶段评估常量表达式并对返回false的表达式断言,我们称这种断言为静态断言。

在GCC上,即使指定使用C++11标准,GCC依然支持单参数的static_assert。MSVC则不同,要使用单参数的static_assert需要指定C++17标准。

第20章 结构化绑定 C++17 C++20

  • 结构化绑定 C++17

要想解决第二个问题就必须使用C++17标准中新引入的特性——结构化绑定。所谓结构化绑定是指将一个或者多个名称绑定到初始化对象中的一个或者多个子对象(或者元素)上,相当于给初始化对象的子对象(或者元素)起了别名,请注意别名不同于引用

结构化绑定能够直接绑定到结构体上。

真实的情况是,在结构化绑定中编译器会根据限定符生成一个等号右边对象的匿名副本,而绑定的对象正是这个副本而非原对象本身。

cpp
auto t = std::make_tuple(42, "hello world");
int x = 0, y = 0;
std::tie(x, std::ignore) = t;
std::tie(y, std::ignore) = t;

结构化绑定可以作用于3种类型,包括原生数组、结构体和类对象、元组和类元组的对象

第22章 类型别名和别名模板 C++11 C++14

  • using C++11

  • 别名模板

using identifier = type-id

identifier是类型的别名标识符,type-id是已有的类型名。

别名模板:

template < template-parameter-list >
using identifier = type-id;
cpp
#include <map>
#include <string>

template<class T>
struct int_map {
  typedef std::map<int, T> type;
};

int main()
{
  int_map<std::string>::type int2string;
  int2string[11] = "7";
}

模板元编程:

cpp
template<bool, typename _Tp = void>
struct enable_if { };

template<typename _Tp>
struct enable_if<true, _Tp>
{ typedef _Tp type; };

第23章 指针字面量nullptr C++11

  • nullptr C++11

NULL,:

cpp
#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0
    #else
        #define NULL ((void *)0)
    #endif
#endif

nullptr 是 std::nullptr_t的纯右值

cpp
namespace std
{
  using nullptr_t = decltype(nullptr);
  // 等价于
  typedef decltype(nullptr) nullptr_t;
}

static_assert(sizeof(std::nullptr_t) == sizeof(void *));

第25章 线程局部存储 C++11

  • thread-local storage thread_local C++11

线程局部存储是指对象内存在线程开始后分配,线程结束时回收且每个线程有该对象自己的实例

线程局部存储的对象都是独立于各个线程的。

在Windows中可以通过调用API函数TlsAlloc来分配一个未使用的线程局部存储槽索引(TLS slot index),这个索引实际上是Windows内部线程环境块(TEB)中线程局部存储数组的索引。通过API函数TlsGetValue与TlsSetValue可以获取和设置线程局部存储数组对应于索引元素的值。API函数TlsFree用于释放线程局部存储槽索引。

Linux使用了pthreads(POSIX threads)作为线程接口,在pthreads中我们可以调用pthread_key_create与pthread_key_delete创建与删除一个类型为pthread_key_t的键。利用这个键可以使用pthread_setspecific函数设置线程相关的内存数据,当然,我们随后还能够通过pthread_getspecific函数获取之前设置的内存数据。

thread_local说明符可以用来声明线程生命周期的对象,它能与static或extern结合,分别指定内部或外部链接,不过额外的static并不影响对象的生命周期。

为了规避由此产生的不确定性,POSIX将errno重新定义为线程独立的变量,为了实现这个定义就需要用到线程局部存储,直到C++11之前,errno都是一个静态变量,而从C++11开始errno被修改为一个线程局部存储变量。

第26章 扩展的inline说明符 C++17

  • inline C++17

定义非常量静态成员变量的问题

C++17标准中增强了inline说明符的能力,它允许我们内联定义静态变量

cpp
#include <iostream>
#include <string>

class X {
public:
  inline static std::string text{"hello"};
};

int main()
{
  X::text += " world";
  std::cout << X::text << std::endl;
}

第27章 常量表达式 constexpr C++11~C++20

  • constexpr    C++11

  • constexpr 增强 C++14

  • if constexpr C++17

  • 允许constexpr虚函数 C++20

  • 允许在constexpr函数中出现Try-catch C++20

  • 允许在constexpr中进行平凡的默认初始化 C++20

  • 允许在constexpr中更改联合类型的有效成员 C++20

  • constinit说明符 C++20

if constexpr的条件必须是编译期能确定结果的常量表达式。

条件结果一旦确定,编译器将只编译符合条件的代码块

和运行时if的另一个不同点:if constexpr不支持短路规则

constinit说明符主要用于具有静态存储持续时间的变量声明上,它要求变量具有常量初始化程序。

第29章 字面量优化 C++11 ~C++17

  • 二进制整数字面量0b 0B C++14

  • 单引号作为整数分隔符 C++14

  • 原生字符串字面量 C++11

  • 用户自定义字面量 C++11

单引号整数分隔符对于十进制、八进制、十六进制、二进制整数都是有效的

cpp
constexpr int x = 123'456;
static_assert(x == 0x1e'240);
static_assert(x == 036'11'00);
static_assert(x == 0b11'110'001'001'000'000);

最简单的原生字符串字面量声明是R"(raw_characters)"

cpp
char hello_world_html[] = R"cpp(<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
  <title>Hello World!</title>
</head>
<body>
"(Hello World!)"
< / body >
< / html>
)cpp";

第30章 alignof 和 alignas C++11 C++17

  • alignas说明符 C++11

  • alignof运算符 C++11

alignof运算符可以用于获取类型的对齐字节长度,alignas说明符可以用来改变类型的默认对齐字节长度。

在alignof运算符被引入之前,程序员常用offsetof来间接实现alignof的功能:

cpp
#define ALIGNOF(type, result) \
  struct type##_alignof_trick{ char c; type member; }; \
  result = offsetof(type##_alignof_trick, member)

int x1 = 0;
ALIGNOF(int, x1);
cpp
template<class T> struct alignof_trick { char c; T member; };
#define ALIGNOF(type) offsetof(alignof_trick<type>, member)

auto x1 = ALIGNOF(int);
auto x2 = ALIGNOF(void(*)());

C++11标准除了提供了关键字alignof和alignas来支持对齐字节长度的控制以外,还提供了std::alignment_of、std::aligned_storage和std::aligned_union类模板型以及std::align函数模板来支持对于对齐字节长度的控制。

第31章 属性说明符和标准属性 C++11 ~ C++20

  • 属性说明符 C++11

  • noreturn C++11

  • carries_dependency C++11

  • deprecated C++14

  • fallthrough C++17

  • nodiscard C++17

  • maybe_unused C++17

  • likely unlikely C++20

  • no_unique_address C++20

GCC的属性语法:

_attribute__((attribute-list))

MSVC的属性语法:

__declspec(attribute-list)

C++11标准的属性:

[[attr]] [[attr1, attr2, attr3(args)]] [[namespace::attr(args)]]
  • noreturn noreturn是C++11标准引入的属性,该属性用于声明函数不会返回

  • carries_dependency 该属性允许跨函数传递内存依赖项,它通常用于弱内存顺序架构平台上多线程程序的优化,避免编译器生成不必要的内存栅栏指令。

  • deprecated 是在C++14标准中引入的属性,带有此属性的实体被声明为弃用

    cpp
    [[deprecated("foo was deprecated, use bar instead")]] void foo() {}
    void bar() {}
    int main()
    {
      foo();
    }
  • fallthrough是C++17标准中引入的属性,该属性可以在switch语句的上下文中提示编译器直落行为是有意的

  • nodiscard是在C++17标准中引入的属性,该属性声明函数的返回值不应该被舍弃,否则鼓励编译器给出警告提示。

  • maybe_unused是在C++17标准中引入的属性,该属性声明实体可能不会被应用以消除编译器警告。

  • likely和unlikely是C++20标准引入的属性,两个属性都是声明在标签或者语句上的。likely属性允许编译器对该属性所在的执行路径相对于其他执行路径进行优化;而unlikely属性恰恰相反。

  • no_unique_address是C++20标准引入的属性,该属性指示编译器该数据成员不需要唯一的地址,也就是说它不需要与其他非静态数据成员使用不同的地址。

第32章 新增预处理器和宏 C++17 C++20

  • 预处理器 __has_include C++17

  • 属性特性测试宏 __has_cpp_attribute C++20

  • 语言功能特性测试宏 C++20

  • 标准库功能特性测试宏 C++20

  • 宏__VA_OPT__ C++20

__has_include用于判断某个头文件是否能够被包含进来

cpp
#if __has_include(<optional>)
#  include <optional>
#  define have_optional 1
#elif __has_include(<experimental/optional>)
#  include <experimental/optional>
#  define have_optional 1
#  define experimental_optional 1
#else
#  define have_optional 0
#endif

属性测试宏(__has_cpp_attribute)可以指示编译环境是否支持某种属性,该属性可以是标准属性,也可以是编译环境厂商特有的属性。

cpp
std::cout << __has_cpp_attribute(deprecated); // 输出结果如下:201309
属性
carries_dependency200809L
deprecated201309L
fallthrough201603L
likely/unlikely201803L
maybe_unused201603L
no_unique_address201803L
nodiscard201603L
noreturn200809L

第34章 基础特性的其他优化 C++11~C++20

  • 显式自定义类型转换运算符 explicit C++11

  • 返回值优化 C++11

  • 支持new表达式推导数组长度 C++20

  • 模块module C++20

怎么感觉C++20的一些特性有点naoc....

返回值优化是C++中的一种编译优化技术,它允许编译器将函数返回的对象直接构造到它们本来要存储的变量空间中而不产生临时对象。

RVO(Return Value Optimization)和NRVO(Named Return Value Optimization)

C++20之前,下面的代码就无法成功编译了:

cpp
int *x = new int[]{ 1, 2, 3 };
char *s = new char[]{ "hello world" };

模块(module)是C++20标准引入的一个新特性,它的主要用途是将大型工程中的代码拆分成独立的逻辑单元,以方便大型工程的代码管理。

第35章 可变参数模板 C++11 C++17 C++20

  • 可变参数模板 C++11

  • using声明中的包展开 C++17

在类模板中,模板形参包必须是模板形参列表的最后一个形参

对于函数模板而言,模板形参包不必出现在最后,只要保证后续的形参类型能够通过实参推导或者具有默认参数即可

cpp
template<class …T>
int baz(Tt)
{
  return 0;
}

template<class …Args>
void foo(Argsargs) {}

template<class …Args>
class bar {
public:
  bar(Argsargs)
  {
       foo(baz(&args…) + args…);
  }
};

int main()
{
  bar<int, double, unsigned int> b(1, 5.0, 8);
}第一个部分是对函数模板baz(&args…)的解包,其中&args…是包展开,&args是模式,这部分会被展开为baz(&a1, &a2, &a3);第二部分是对foo(baz(&args…) + args…)的解包,由于baz(&args…)已经被解包,因此现在相当于解包的是foo(baz(&a1, &a2, &a3) +args…),其中baz(&a1, &a2, &a3) + args…是包展开,baz(&a1, &a2, &a3) + args是模式,最后的结果为foo(baz(&a1, &a2, &a3) + a1, baz(&a1, &a2, &a3) + a2,baz(&a1, &a2, &a3) + a3)ba
template<class F, class… Args>
auto delay_invoke(F f, Args… args) {
    return [f, args…]() -> decltype(auto) {
        return std::invoke(f, args…);
    };
}

上面这段代码实现了一个delay_invoke,目的是将函数对象和参数打包到一个lambda表达式中,等到需要的时候直接调用lambda表达式实例,而无须关心参数如何传递。

sizeof…是专门针对形参包引入的新运算符,目的是获取形参包中形参的个数,返回的类型是std::size_t

第37章 模板参数优化 C++11 C++17 C++20

  • 允许常量求值作为所有非类型模板的实参 C++11

  • 允许局部和匿名类型作为模板实参 C++11

第39章 用户自定义推到指引 C++17

  • 自定义推导指引推导模板实例 C++17

对于std::make_pair来说,从C++11开始它使用std::decay主动让数组类型衰退为指针,而在C++11之前,它用传值的办法来达到让数组类型衰退为指针的目的。

cpp
template<typename _T1, typename _T2> pair(_T1, _T2) -> pair<_T1, _T2>;

第40章 SFINAE C++11

  • SFINAE(Substitution Failure Is Not An Error,替换失败不是错误)C++11
cpp
template<bool B, class T = void>
struct enable_if {};

template<class T>
struct enable_if<true, T> { using type = T; };