C++11特性的学习之提高性能及操作硬件的能力(五)

  目录括号内为适合人群,所有库作者的内容暂不做学习,可自行查阅《深入理解C++11:C++11新特性解析与应用》。网盘链接: https://pan.baidu.com/s/1Jf29R7-foOoXJ5UW3mTKVA 密码: 7vgq

目录

1.常量表达式(类作者)
  ①运行时常量性与编译时常量性
  ②常量表达式函数
  ③常量表达式值
  ④常量表达式的其他应用
2.变长模板(库作者)
  ①变长函数和变长的模板参数
  ②变长模板:模板参数包和函数参数包
  ③变长模板:进阶
3.原子类型与原子操作(所有人)
  ①并行编程,多线程与C++11
  ②原子操作与C++11原子类型
  ③内存模型,顺序一致性与memory_order
4.线程局部存储(所有人)
5.快速退出:queck_exit与at_quick_exit(所有人)

1.常量表达式 ^

  ①运行时常量性与编译时常量性 ^

  常量表示该值不可修改,通常是通过const关键字修饰的。如:

const int i = 5;

  const可以修饰函数参数,函数返回值,函数本身,类等,在不同的使用条件下,const有不同的意义,不过大多数情况下,const描述的都是一些“运行时常量性”的概念,即运行时数据的不可更改性。不过有的时候,我们需要的却是编译时期的常量性,这是const关键字无法保证的。如:

const int getConst1() { return 1; }     //运行时常量性
constexpr int getConst2() { return 1; } //编译时常量性

int main()
{
    int arr[getConst1()] = { 0 };    //编译无法通过
    int arr2[getConst2()] = { 0 };   //编译通过
}

  在C++11标准中,我们在函数表达式前加上constexpr关键字声明为常量表达式即可讲函数描述为”编译时常量性“。在C++11中,常量表达式可以作用的实体不仅限于函数,还可以作用于数据声明,以及类的构造函数等。

  ②常量表达式函数 ^

  通常我们可以在函数返回类型前加入关键字constexpr来使其成为常量表达式函数。不过并非所有的函数都有资格成为常量表达式函数,有以下几点要求:

  • 函数体只有单一的return返回语句
  • 函数必须有非void返回值
  • 在使用前必须已有定义
  • return返回语句表达式中不能使用非常量表达式的函数,全局数据,且必须是一个常量表达式。
  •   对于第一个要求,就是要求函数体中只有一条语句,且该条语句必须是return语句(VS2017中,就算多条语句编译也通过),如:

    //VS2017编译通过,原书籍意思是编译器不会通过。
    constexpr int getConst() { const int i = 5; return i; } 

      对于第四个要求,如下:

    //const int getConst1() { return 1; }
    //constexpr int getConst2() { return getConst1(); }
    
    int a=5;
    constexpr int getConst() {return a;}
    int main()
    {
        //编译器不通过
        //cout<<getConst2()<<endl;
        cout<<getConst()<<endl;  //原书籍举出的这个例子VS2017编译通过了
    }

      ③常量表达式值 ^

      通常情况下,常量表达式值必须被一个常量表达式赋值,而跟常量表达式函数一样,常量表达式值在使用前必须被初始化。在C++11标准中,constexpr关键字不能用于修饰自定义类型的定义的。只能定义自定义常量构造函数。如下:

    class A
    {
    public:
        constexpr A():a(10) { }
    private:
        int a;
    };

      常量表达式的构造函数有以下约束:

  • 函数体必须为空(VS2017函数体不为空也通过编译)
  • 初始化列表只能由常量表达式来赋值
  • 由于原书籍的例子与本人调试结果出入大,暂不深究

      ④常量表达式的其他应用 ^

      原书籍p180有3个应用例子

    2.变长模板 ^

      暂不作学习,可查阅《深入理解C++11:C++11新特性解析与应用》。

      ①变长函数和变长的模板参数 ^

      ②变长模板:模板参数包和函数参数包 ^

      ③变长模板:进阶 ^

      

    3.原子类型与原子操作 ^

      

      ①并行编程,多线程与C++11 ^

      在C++11之前,C/C++一直是一种顺序的编程语言,即所有指令都是串行执行的。随着多核处理器的发展,编程语言也逐渐开始向并行化的编程方式发展。

      常见的并行编程有多种模型,如共享内存,多线程,消息传递等。从实用性上讲,多线程模型具有较大的优势,多线程模型允许同一时间有多个处理器单元执行统一进程中的代码部分,而通过分离的栈空间和共享的数据区及堆栈空间,线程可以拥有独立的执行状态以及进行快速的数据共享。

      在C++11标准中,引入了多线程的支持,使得C/C++语言在进行线程编程时,不必依赖第三方库和标准。而C/C++对线程的支持,一个最为重要的部分,就是在原子操作中引入了原子类型的概念。

      ②原子操作与C++11原子类型 ^

      所谓原子操作,就是多线程程序中“最小的且不可并行化的”操作。通常对一个共享资源的操作是原子操作的话,意味着多个线程访问该资源时,有且仅有唯一一个线程在对这个资源进行操作。从线程的角度看来,其他线程就不能够在本线程对资源访问期间对该资源进行操作。通常情况下,原子操作是通过“互斥”的访问来保证的,实现互斥通常需要平台相关的特殊指令,在C++11标准之前,意味着需要在C/C++11代码中嵌入内联汇编代码,而C++11标准中,通过对并行编程的抽象处理上述问题。如下:

    #include<thread>
    #include<atomic>
    #include<iostream>
    using namespace std;
    
    atomic_int sum{ 0 };//原子数据类型,等同于int
    
    void func(int)
    {
    
        for (int i = 0; i != 20000; ++i)
            sum += i;
    }
    int main()
    {
        thread t1(func, 0);
        thread t2(func, 0);
        t1.join();
        t2.join();
        cout << sum << endl;   //输出结果为399980000
    }

      在C++11中,我们不需要为原子数据类型显式地声明互斥锁或调用加锁,解锁的API,线程就能够对变量sum互斥地进行访问。使用原子类型我们只需要包含头文件atomic就行了,而原子类型的名称诸如atomic_int,atomic_char,atomic_uint这样。不过更为方便的,我们可以使用atomic类模板,通过该类模板我们可以定义出任意需要的原子类型。如:

    atomic<int> t;   //声明了int类型的原子类型

      对于线程而言,原子类型通常属于“资源型”的数据,这意味着多个线程通常只能访问单个原子类型的拷贝。因此在C++11中,原子类型只能从其模板类型中进行构造,标准不允许原子类型进行拷贝构造,移动构造,以及使用operator=等。如:

    atomic<int> a{ 1 };
    atomic<int> b{ a };//无法通过编译

      在C++11标准中,将原子操作定义为atomic模板类的成员函数,对大多数的原子类型而言,都可以执行读,写,交换,比较并交换等操作。如下:

    atomic<int> a{ 2 };
    //原子操作可以避免线程间关于a的竞争
    a=10;               //等同于a.store(10);
    int b = a;          //等同于b=a.load();

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

      在C++11中的原子类型的变量在线程中总是保持顺序执行的特性。我们称这样的特性为“顺序一致”的,即代码在线程中运行的顺序与我们看到的代码顺序一致。顺序一致只是属于C++11多种内存模型中的一种。对C++11中的内存模型而言,要保证代码的顺序一致性,就必须同时做到以下几点:

  • 编译器保证原子操作的指令间顺序不变,即保证产生的读写原子类型的变量的机器指令与代码编写者看到的是一致的。
  • 处理器对原子操作的汇编指令的执行顺序不变。
  •   C++11标准中还有松散的内存模型,就是我们会原子操作指定所谓的内存顺序:memory_order。memory_order作为一个参数,在C++11标准中一共定义了6种枚举值:

    枚举值 定义规则
    memory_order_relaxed 不对执行顺序做任何保证
    memory_order_acquire 本线程中,所有后续的读操作必须在本条原子操作完成后执行
    memory_order_release 本线程中,所有之前的写操作完成后才能执行本条原子操作
    memory_order_acq_rel 同时包含memory_order_acquire和memory_order_release标记
    memory_order_consume 本线程中,所有后续的有关本原子类型的操作,必须在本条原子操作完成之后执行
    memory_order_seq_cst 全部存取都按顺序执行(默认值)

      通常情况下,我们可以把atomic成员函数可使用的memory_order值分为以下3组:

  • 原子存储操作(store)可以使用memory_order_relaxed,memory_order_release,memory_order_seq_cst。
  • 原子读取操作(load)可以使用memory_order_relaxed,memory_order_consume,memory_order_acquire,memory_order_seq_cst
  • RMW操作,即一些需要同时读写的操作。可以使用全部6种操作。
  •   具体的使用例子可查阅原书籍P210

    4.线程局部存储 ^

      线程局部存储(TLS),就是拥有线程生命期及线程可见性的变量。线程局部存储实际上是由单线程程序中的全局/静态变量被应用到多线程程序中被线程共享而来。

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

    int thread_local errCode;

      由于打不开pthread.h,具体的可查阅原书籍P216

    5.快速退出:queck_exit与at_quick_exit ^

      在C++程序中,我们常常会看到一些有关“终止”的函数,如terminate,abort,exit等。对普通的程序来说,它们都只是终止程序的运行而已,不过实际上,它们对应着“正常退出”和”异常退出“两种情况。terminate函数是C++语言中异常处理的一部分,一般而言,没有被捕捉的异常就会导致terminate函数的调用。abort函数不会调用任何的祈构函数,一般是系统在毫无办法下的无奈之举–终止进程。exit则是使程序“正常退出”。

      在C++11标准中,引入了quick_exit函数,该函数并不执行祈构函数而只是使程序终止,与exit同属于“正常退出”。此外,使用at_quick_exit注册的函数也可以在quick_exit的时候被调用。不过标准要求编译器至少支持32个注册函数的调用。如下:

    class A
    {
    public:
        A() { cout << "构造函数" << endl; }
        ~A() { cout << "祈构函数" << endl; }
    };
    
    void func() { cout << "注册的函数" << endl; }
    
    int main()
    {
        A a;
        //①当没有quick_exit而由at_quick_exit函数时,输出:构造函数  祈构函数。可以看出at_quick_exit并没有执行而祈构函数执行了。
        //②当queck_exit和at_queck_exit两者都有时,输出:构造函数,注册的函数。可以看出祈构函数没有执行,而at_queck_exit执行了。
        at_quick_exit(func);
        quick_exit(0);
    }

    猜你喜欢

    转载自blog.csdn.net/qq_17044529/article/details/82477284