X

曜彤.手记

随记,关于互联网技术、产品与创业

  1. 一、迈向现代 C++
  2. 二、语言可用性的强化
  3. 三、语言运行期的强化
  4. 四、容器
  5. 五、智能指针与内存管理
  6. 六、正则表达式
  7. 七、并行与并发

《现代 C++ 教程:高速上手 C++11/14/17/20》读书笔记

又一本知乎上推荐的 C++ 书籍,只有不到 100 页,因此可以用来花点时间快速查缺补漏一下。

一、迈向现代 C++

  1. Page 8C++11 以后被弃用的特性
  • 不再允许将字符串字面值常量赋值给一个 char*
  • C++98 异常说明unexpected_handlerset_unexpected() 等相关特性被弃用,应该使用 noexcept;
  • auto_ptr 被弃用,应使用 unique_ptr;
  • register 关键字被弃用,可以使用但不再具备任何实际含义;
  • bool 类型的 “++” 操作被弃用;
  • 如果一个类有析构函数,为其生成默认拷贝构造函数和拷贝赋值运算符的特性被弃用了;(存疑)
  • C 语言风格的类型转换被弃用;
  • C++17 中弃用部分原有的 C 标准库,<ccomplex>、<cstdalign>、<cstdbool> 与 <ctgmath> 等;
  • C++11 提供的 std::bindstd::function
  1. Page 9在不得不使用 C 时,应该注意使用 extern "C" 这种特性,将 C 语言的代码与 C++ 代码进行分离编译,再统一链接这种做法。
// foo.h
#ifdef __cplusplus
extern "C" {
#endif
int add(int x, int y);
#ifdef __cplusplus
}
#endif
// foo.c
int add(int x, int y) {
  return x + y;
}
// main.cc
#include "foo.h"
#include <iostream> 
#include <functional>
int main(int argc, char **argv) {
  // out 为一个自动推导的 const 引用.
  [out = std::ref(std::cout << "Result from C code: " << add(1, 2))](){
    out.get() << ".\n";
  }();
  return 0;
}
  1. Page 10C 和 C++ 互相兼容情况

二、语言可用性的强化

  1. Page 10语言可用性:指那些发生在运行时之前的语言行为。
  2. Page 12传统 C++ 会把 NULL、0 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义为 0。而C++ 不允许直接将 void* 隐式转换到其他类型
  3. Page 12nullptr 的类型为 nullptr_t,能够隐式转换为任何指针或成员指针的类型,也能和他们进行相等性上的比较
  4. Page 15从 C++14 开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句:
constexpr int fib(const int n) {
  if (n == 1) return 1;
  if (n == 2) return 1;
  return fib(n - 1) + fib(n - 2);
}
  1. Page 16C++17 消除了对 if/switch 变量声明位置(可以在结构内部声明)的限制:
std::vector<int> vec = {1, 2, 3, 4};
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end()) {
  *itr = 4;
}
  1. Page 18列表初始化”与“结构化绑定”:
#include <iostream>
#include <tuple>

std::tuple<int, double, std::string> foo() {
  return std::make_tuple(1, 2.3, "456");
}
int main(int argc, char** argv) {
  auto [x, y, z] = foo();
  std::cout << x << ", " << y << ", " << z << std::endl;
  return 0;
}
  1. Page 18自动类型推导:
auto arr = new auto(10);  // arr -> int*.
template<typename T, typename U>
auto add(T x, U y) {  // auto add(T x, U y) -> decltype(x + y) {.
  return x + y;
}
  • auto 不能用于函数参数的类型推导(与重载冲突)。也不能用于数组类型的推导
  • decltype 是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的;
  • decltype(auto) 主要用于对转发函数或封装的返回类型进行推导(让 auto 以 decltype 的方式进行类型推导,并同时保留顶层 CV 标识符)。decltype 推导规则:
    • 如果 e 是一个没有带括号的标记符(一般为程序员自定义的标记)表达式或类成员访问表达式,则推导结果为 e 所命名的实体类型;
    • 否则,若 e 的类型为 T,若 e 为将亡值 xvalue(一般为 std::move(x);而字面量值通常为纯右值 prvalue),则推导结果为 T&&;
    • 否则,若 e 的类型为 T,若 e 为一个左值(引用),则推导结果为 T&;
    • 否则,若 e 的类型为 T,推导结果为 T;
std::string& foo();
decltype(auto) bar() {
  return foo();
}
  1. Page 22C++17 允许在代码中声明常量表达式(constexpr)判断条件
template<typename T>
auto foo(const T& t) {
  if constexpr (std::is_integral<T>::value) {
    return t + 1;
  } else {
    return t + 0.001;
  }
}
  1. Page 24传统 C++ 中,模板只有在使用时才会被编译器实例化。换句话说,只要在每个“编译单元”中,编译的代码中遇到了被完整引用的模板,都会进行实例化,而重复实例化便会导致编译时间的增加。对于这个问题可以通过“外部模板”来解决(缩短编译时间减小中间 “.o” 文件的大小)。
extern template class std::vector<double>;
  1. Page 25模板是用来产生类型的typedef 仅能用来定义类型,而无法定义模板别名。这里使用 using 便可以做到“模板别名”:
template<typename T>
using Foo = Bar<std::vector<T>, std::string>
  1. Page 26默认模板参数
template<typename T = int, typename U = int>
auto add(T x, U y) {
  return x + y;
}
  1. Page 26变长模板参数
  • 普通递归模板展开:
template<typename T>
void foo(T arg) {
  std::cout << arg << std::endl;
}
template<typename T, typename... Ts>
void foo(T arg, Ts... args) {
  std::cout << arg << std::endl;
  foo(args...);
}
  • C++17 参数模板展开:利用 constexpr 进行编译时的模板条件展开。
template<typename T, typename... Ts>
void foo(T arg, Ts... args) {
  std::cout << arg << std::endl;
  if constexpr (sizeof...(args) > 0) foo(args...);
}
  • 初始化列表展开:通过初始化列表,(lambda, value)... 将会被展开。由于逗号表达式的出现,首先会执行前面的 lambda 表达式,完成参数的输出。
template<typename T, typename... Ts>
void foo(T arg, Ts... args) {
  std::cout << arg << std::endl;
  (void) std::initializer_list<T>{
    ([&args]{
      std::cout << args << std::endl;
    }(), arg)...
  };
}
  • C++17 通过折叠表达式(可代替上述“初始化列表”):
template<typename... Ts>
void foo(Ts... args) {
  auto _t = [](auto arg) {
    std::cout << arg << std::endl;
  };
  (_t(args), ...);  // comma expression.
}
  1. Page 28非类型模板参数
template<auto size>
void foo() {
  int arr[size] = {};
  std::cout << (sizeof(arr) / sizeof(int)) << std::endl;
}
int main(int argc, char** argv) {
  foo<10>();
  return 0;
}
  • C++17 可以使用 auto 来辅助完成对字面量值类型(“size”)的推导过程;
  1. Page 29委托构造:
class A {
 public:
  int v;
  A() = default;
  A(int x) : A() {  // 委托另一个构造函数;
    v = x;
  }
};
  1. Page 30继承构造:
class A {
 public:
  int v;
  A() = default;
  A(int x) : A() { v = x; }
};
class B : A {
 public:
  using A::A;  // 继承基类的构造函数;
};
  1. Page 31fianl 关键字:防止类被继续继承以及终止虚函数继续重载override 关键字:显式告知编译器进行重载,防止错误重载的发生。
class A final {
  virtual void foo() final {
    std::cout << "A" << std::endl;
  }
};
  1. Page 32=default=delete
struct A {
  A() = default;
  A& operator=(const A&) = delete;
};

三、语言运行期的强化

  1. Page 34Lambda 表达式:唯一需要注意的是,Lambda 表达式的“值拷贝”是在定义时拷贝的。C++14 允许捕获的成员用任意的表达式进行初始化,被声明的捕获变量类型会根据表达式进行判断。
int main(int argc, char** argv) {
  auto x = std::make_unique<int>(1);
  auto y = [v = std::move(x)]() {  // “v” 使用表达式进行初始化;
    return *v + 10;
  };
  std::cout << y() << std::endl;
}
  • C++14 Lambda 泛型
auto add = [](auto x, auto y) {
  return x + y;
}
  1. Page 36当 Lambda 表达式的捕获列表为空时,闭包对象还能够转换为函数指针进行传递。C++11 中所有“可调用对象”都可以通过 std::function 进行表示,并且可以同 std::bind 一起使用来绑定部分参数。
  2. Page 40“常量左值引用”为万能引用类型,可以引用任何类型的右值。而常量左值引用允许绑定到右值则源于 Fortran 的需要
  3. Page 43引用折叠:对一个引用类型继续进行引用。无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型

  • 完美转发:使用 std::forward 来保持参数传递后的“右值性”;
  • 在使用循环语句的过程中,auto&&最安全的方式(当 auto 被推导为不同的左右引用时,与 && 的引用折叠组合是完美转发);
int main(int argc, char** argv) {
  std::vector<int> v{1, 2, 3};
  for (auto&& i : v) {  // int&.
    std::cout << i << std::endl;
  }
}
  • std::movestd::forward 的简单实现:
// std::forward 的简单实现;
template<typename T>                         
T&& forward(std::remove_reference_t<T>& param) {  
  return static_cast<T&&>(param);
}
// std::move 的简单实现;
template<typename T>
std::remove_reference_t<T>&& move(T&& t) {
  return static_cast<std::remove_reference_t<T>&&>(t);
}

四、容器

  1. Page 47同传统数组一样,std::array 的数组大小参数也必须是常量表达式:
constexpr int len = 4;
std::array<int, len> = {1, 2, 3, 4};
  1. Page 48C++ 中的有序容器 std::mapstd::set,这些元素内部通过红黑树进行实现,元素插入和搜索的平均复杂度均为 “O(log(size))”。而对应的无序容器则使用“Hash 表”实现,对应的操作复杂度为常数级。
  2. Page 51C++17 运行时遍历 std::tuple
int main() {
  std::tuple t{42, 'a', 4.2}; 
  std::apply(
    [](auto&&... args) {  // 参数为该 Tuple 的所有元素;
      ((std::cout << args << '\n'), ...);
    }, t);
}

五、智能指针与内存管理

  1. Page 54C++11 为什么没有提供 std::make_unique?Herb Sutter 答:“被他们忘记了”。

六、正则表达式

(略)

七、并行与并发

  1. Page 62std::thread 用于创建一个执行的线程实例:
#include <thread>
int main() {
  std::thread t([](){
    std::cout << "Hello, world!" << std::endl;
  });
  t.join();
  return 0;
}

(其他略,暂时用不到)




评论 | Comments


Loading ...