现代C++实战30讲(2)

本文记录学习吴咏炜老师的《现代C++实战》课程的心得体会,记录自认为值得记录的知识点。

重新认识的点

  1. 编译期间的多态

    所有容器类都有 beginend 函数成员,这为通用遍历容器提供可能。很多容器都有 size 成员,没有继承通用的类。虽然C++标准容器之间没有对象继承关系,但彼此之间存在很多共性,C++中有模板来表达这类共性。

    显示实例化: template class vector
    外部实例化: extern template class vector; 告诉编译器此模板已在其他地方实例化,此处无需再实例化。
    上述两项实例化可在大型项目中集中模板的实例化,加速编译过程,但也有额外开销,应当谨慎使用。

    要求不同的对象拥有共同的成员函数,名字和参数结构都要相同。

    如果要在共性中有针对特殊类型的特殊处理,对函数模板使用重载,对类模板使用特化。

    基于虚函数的多态,解决的是运行时的行为变化,强调对流程的复用。
    基于模板的多态,解决的是同一套代码适用于不同的数据类型,强调对代码的复用。

  2. 模板元编程
    最核心的一点,需要把计算转变为类型推导,具体来说是用编译器的类型推导和类型匹配来表达计算过程。

  3. 常量表达式
    一个 constexpr 常量是一个编译期间完全确定的常数。

  4. Lambda表达式
    Lambda表达式一般不需要说明返回值,相当于auto;有特殊情况时,使用后置箭头语法说明: [](int x) -> int

    auto adder = [](int n){
    	return n + 2;
    }
    adder(2);
    
    // 或者更简洁的立即求值
    [](int x){return x*x}(3);
    
    // 根据不同模式,进行不同处理。 不要求 obj 有默认无参构造函数,又省却了拷贝开销。
    auto obj = [init_mode](){
    	switch(init_mode)
    	{
    		case init_mode1: return obj(xxx);
    		case init_mode2: return obj(xxx2);
    	}
    }();
    

    显式的代码比隐式的代码更容易维护。一般采用按值捕获,如果是按引用捕获,必须小心确保被捕获变量和lambda表达式的生命周期至少一样长
    并且在满足下列条件之一时才使用:

    1. 需要在lambda表达式中修改变量并让外部观察到
    2. 需要看到这个变量在外部被修改的结果
    3. 这个变量的复制代价比较高

    如果是捕获指针,则要保证指针指向的资源生命周期至少和Lambda表达式的一样长。
    Lambda表达式从概念上,可理解为一个匿名struct,它实现了 operator() 运算符,支持调用,捕获过程就是给struct中的成员变量赋值的过程

  5. 函数式编程
    标准算法库中有

    • transform,把一个范围内的每个对象经过相同的处理,变成另外一系列对象
    • accumulate,把一个范围内的对象,使用指定的初始值和处理方式,进行归并(默认的处理方式为+,也就是累加)
    • copy_if, 把一个范围内的对象,提取满足指定条件的对象集合
    • partition,把一个范围内的对象,根据过滤条件,分为两组

    transform函数可通过在调用中加入 execution::par 来启动并行计算。 accumulate 可用 reduce 来替换,并启动并行计算
    获得多核环境下的性能提升。C++17中才有。

  6. 多线程编程

    • thread要求在线程变量析构前,要么通过join(阻塞直到线程推出),要么detach(放弃对线程的管理),否则程序会异常退出。
    mutex output_lock;
    
    void func(const char* name)
    {
    	this_thread::sleep_for(100ms);
    	lock_guard<mutex> guard{ output_lock };
    	cout << "I am thread " << name << this_thread::get_id() <<endl;
    }
    
    // 包装一下 thread,使得能在析构时,自动调用 join 接口
    class scoped_thread {
    public:
    	// 通过可变模板和完美转发来构造
    	template<typename... Arg>
    	scoped_thread(Arg&&... arg)
    		:thread_(std::forward<Arg>(arg)...) {}
    
    	// 可以移动
    	scoped_thread(scoped_thread&& other)
    		:thread_(std::move(other.thread_)) {}
    
    	// 不能拷贝
    	scoped_thread(const scoped_thread&) = delete;
    
    	~scoped_thread()
    	{
    		// 只有可joinable的才能调用join
    		if (thread_.joinable())
    		{
    			thread_.join();
    		}
    	}
    
    private:
    	thread thread_;
    };
    
    void test_thread()
    {
    #if 0
    	thread t1{ func , "A" };
    	thread t2{ func , "B" };
    	t1.join();
    	t2.join();
    #endif
    
    	scoped_thread t1{ func, "A" };
    	scoped_thread t2{ func, "B" };
    }
    
    
  7. 内存模型
    编译器为了优化,可以调整代码执行顺序,保证外部可观测行为一致
    处理器的乱序执行,也会调整代码执行顺序。

    volatile的语义时防止编译器优化对内存的读写,每次都去读写内存,在多处理器环境下,无法保证多个线程能看到同样顺序的数据变化。

    c++提供原子对象(atomic)以及对应的获得(acquire)、释放(release)语义,可精确控制内存访问顺序。

    • 获得是一个对内存的操作,当前线程的任何后面的读写操作都不允许重排到这个操作前面去。
    • 释放是对一个内存的操作,当前线程的任何前面的读写操作都不允许重排到这个操作的后面去。

    atomic变量的写操作,默认是释放语义;读操作默认是获得语义。但是,缺省行为可能是对性能不利的,并不需要在任何情况下都保证操作的顺序性。

    原子操作有三类:

    • 读: 在读取过程中,读取位置内容不会发生任何变动
    • 写: 在写入过程中,其他执行线程不会看到部分写入结果
    • 读-修改-写入:读取内存、修改数值、然后写会内存,整个操作过程中间不会有其他写入操作插入、其他执行线程不会看到部分写入结果。

    image.png

  8. C++编译器
    CLang作为LLVM项目的一部分,现在成为通用跨平台编译器,它在错误信息易用性上,相比于GCC有极大改善。另外,Clang上扩展新功能很容易,有很多流行的生态组件:

    • 代码自动完成 clang-complete(Vim插件)
    • 代码格式化 clang-format 内置的风格格式需要根据实际情况调整
    • 代码静态检查 clang-tidy

    CLang在Windows平台上会使用MSVC的C++运行时,在Linux上使用libstdc++,在macOS上,才使用CLang的C++库,libc++

  9. 辅助小工具

  10. 多数值对象
    在没有optional之前,多数值对象通常采用如下实现方式:

    
    struct FloatIntChar {
    	enum ValueType{
    		FLOAT,
    		INT,
    		CHAR,
    		STRING,
    	}type;	// 表示实际存储的数据类型
    
    		// 采用联合体,公用同一存储空间
    	union {
    		float fValue;
    		int nValue;
    		char cValue;
    		//string str;	// 增加这个会编译失败
    	};
    };
    

    上述做法针对POD类型成员是可行的,一旦引入非POD类型,编译器会看到 union 中使用 string 类型带来构造和析构问题。
    如果使用 c++17中引入的 variant,就会非常干净利落。

    variant<string, int, char> obj{ "hello", 1, 'c' };
    cout << get<string>(obj) << endl;
    
  11. C++工程库介绍

    1. 高精度整数类型计算库 Boost.Multiprecision
      优点:
      • 支持常用运算符,如 +、-、*、/、%
      • 使用基本的 cpp_int 对象不需要预先编译库,只需要Boost的头文件即可
      • 支持整数和字符串构造
      • 提供用户自定义字面量初始化
      • 支持十进制、十六进制输出
    2. 单元测试库 Catch2
    3. 日志库 spdlog
    4. 命令行输入解析库 getopt
    5. 简洁的benchmark库 Celero
  12. Concept
    概念(Concept)是对模板参数的约束条件,符合这类约束条件的都能被该模板实例化。这里的条件不是某个具体类型,比如 int、long,而是满足某种操作的类型集合。

    template<typename N> requires Integer<N>
    N half(N n)
    {
    	return n / 2;
    }
    

    half是一个函数模板,有一个模板参数N,这个N要满足 Integer的概念约束,该函数才能对N进行实例化。

  13. 课后问答

    1. 为什么stackqueuepoptop/front两个函数,而不合并成一个函数?
      解答:如果pop返回元素,在元素拷贝时发生异常,那么这个元素就丢失了,为保证异常安全,pop没有返回值,简化语义,取值使用另外的接口top/front
posted @ 2023-02-03 09:22  浩天之家  阅读(170)  评论(0编辑  收藏  举报