C++11-C++17新特性介绍

发布时间:2023年12月26日


前言

  • 本篇只包含c++11- c++17的新特性, 一些c++基本规定不再赘述
  • 前文 c++规定 记录了c++基本规定
  • 对于这些新特性,笔者一边学习一边做笔记,所以内容有待完善
  • 笔者使用的是 Clion+MinGW 开发环境

提示:以下是本篇文章正文内容,下面案例可供参考

一、{列表初始化}

  • int a=8 等同于 int a{8}

  • int b{} 初始化为0

  • #include <iostream>
    
    int main(int argc, char const *argv[])
    {
        struct Demo
        {
            int a_;
            int b_;
            Demo(std::initializer_list<int> list)
            {
                std::cout << "Demo(std::initializer_list<int> list)" << std::endl;
            }
        };
        struct Demo1
        {
            int a_;
            int b_;
            Demo1(int a, int b)
            {
                a_ = a;
                b_ = b;
                std::cout << "Demo1(int a, int b)" << std::endl;
            }
        };
        struct Demo2
        {
            int a_;
            int b_;
            Demo2(int a, int b)
            {
                a_ = a;
                b_ = b;
                std::cout << "Demo2(int a, int b)" << std::endl;
            }
            Demo2(std::initializer_list<int> list)
            {
                std::cout << "Demo2(std::initializer_list<int> list)" << std::endl;
            }
        };
        // 非聚合体首先考虑 initializer_list 构造函数
        Demo d{1, 2}; //花括号里的数据的类型必须一致
        Demo1 d1({1, 2}); //等同于d1(1,2) d1{1,2} 编译时未发现initializer_list<int>形参 就把括号内的值一一匹配赋给 Demo2(int a, int b)中的a和b了
        Demo2 d2{1, 2};   //编译器会自动生成一个匿名的initializer_list类型的对象 把这条语句转换为 Demo d2({1,2})
        // Demo2 d2(1, 2);//Demo2(int a, int b) 园括号就是直接指明要调用括号内的就是参数
        return 0;
        /*输出 :
        Demo(std::initializer_list<int> list)
        Demo1(int a, int b)
        Demo2(std::initializer_list<int> list) 
          需要注意的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。
       并且,拷贝或赋值一个initializer_list对象不会拷贝列表中的元素,其实只是引用而已,原始列表和副本共享元素。
       和使用vector一样,我们也可以使用迭代器访问initializer_list里的元素
       如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内:*/
    }
    
  • 与数组的关系

    #include <iostream>
    using namespace std;
    template<class T,size_t N>
    T average(const T (&array)[N]) //因为初始化列表是常量 所以要用const 类型接收
    {
        T sum{};
        for(size_t i{};i<N;++i)
            sum+=array[i];
        return sum/N ;
    }
    int main()
    {
        cout<<average({1.0,2.0,3.0,4.0,5.0})<<endl; //T=double N 为 5
        return 0;
    }
    
  • 与new

    int* a = new int { 123 };
    double b = double { 12.12 };
    int* arr = new int[3] { 1, 2, 3 };
    
  • 推荐以后多使用{}来初始化 更加 type-safe

二、auto 与 decltype 与 decltype(auto)

  • auto

    • C++11 赋予 auto 关键字新的含义,使用它来做自动类型推导。也就是说,使用了 auto 关键字以后,编译器会在编译期间自动推导出变量的类型,这样我们就不用手动指明变量的数据类型了。
    • auto基本使用语法:
      auto name = value;
    • 和某些具体类型混合使用
      int  x = 0;
      auto *p1 = &x;   //p1 为 int *,auto 推导为 int
      auto  p2 = &x;   //p2 为 int*,auto 推导为 int*
      auto &r1  = x;   //r1 为 int&,auto 推导为 int
      auto r2 = r1;    //r2 为  int,auto 推导为 int
      //---------------------------------------------------------------
      const int a = 0;
      auto b = a;     //a 为 const int, auto 被推导为 int(const 属性被抛弃)
      auto &c = a;    //a 为 const int, auto 被推导为 const int(const 属性被保留)c 为 const int&类型
      //-------------------------------------------------------------------
      int n = 10;
      int && r2 = std::move(n);
      int &r3 = n;    //r3是一个int &类型
      auto r4 = r3;   //auto 推导为 int  抛弃了左值引用
      auto r5 = r2;   //auto 推导为 int  抛弃了右值引用
      
    • auto不会推断为一个引用类型,而总是推断为一个值类型,这意味着即使将一个引用赋值给auto,值也会被复制
      但可以使用auto& 或const auto&
    • 对 cv 限定符的处理
      • 「cv 限定符」是 const 和 volatile 关键字的统称:
      • volatile它用来表示数据是可变的、易变的,目的是不让 CPU 将数据缓存到寄存器,而是直接存取原始内存地址
      • 如果表达式的类型不是指针或者引用,auto 会把 cv 限定符直接抛弃
      • 如果表达式的类型是指针或者引用,auto 将保留 cv 限定符。
    • auto的使用限制:
      • auto 不能在函数的参数中使用
        >.warning: use of 'auto' in parameter declaration only available with '-std=c++20' or '-fconcepts'
        >. 我开玩笑的,其实能用 但 auto 和函数参数默认值不能用在同一个参数上(该函数参数默认性将失效)
      • auto 不能作用于类的非静态成员变量
      • auto 关键字不能定义数组
      • auto 不能作用于模板的类型参数
    • 从c++ 17起,可以使用auto来声明一个非类型模板参数。
  • decltype

    • 在某些特殊情况下auto用起来非常不方便,甚至压根无法使用,所以 decltype 关键字也被引入到 C++11 中。

    • decltype基本使用语法:
      decltype(exp) varname = value;
      其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。

    • auto 要求变量必须初始化,而 decltype 不要求:
      decltype(exp) varname;

    • exp 是一个普通的表达式,它可以是任意复杂的形式,但要保证 exp 的结果是有类型的,不能是 void;

    • 如果 exp 是一个左值 或者被括号()包围:decltype((exp)),那么 返回类型就是 exp 的引用;

    • exp为函数调用表达式时需要带上括号和参数, 这仅仅是形式, 并不会真的去执行函数代码。
      decltype(func(100)) 返回的类型就和函数返回值的类型一致

    • 不同于auto, decltype 会保留 cv 限定符 , 以及引用

    • 和某些具体类型混合使用

      int  x = 1;
      int &&u= 5;
      decltype(u)& y=x; //引用折叠 y为 int& 类型
      
  • decltype(auto)

    • 和auto用法一样 不同处在于 会保留 cv 限定符 , 以及引用 且不能和其他类型混合使用
    • 其他用法

      int a=5;
      decltype(auto) b=(a); //b推导为 int& 类型

三、拖尾返回类型 (trailing-return-type)

  • 函数的返回类型推断是在c++14中引入的,那么之前如何解决呢?

    template <typename T, typename U>
    auto add(T t, U u) -> decltype(t + u)
    {
    return t + u;
    }

  • 因为在decltype()中重复函数体内的表达式很枯燥,所以c++14引入了decltype(auto)语法:
    >.效果同上 等于是上面的简写形式

    template <typename T, typename U>
    decltype(auto) add(T t, U u)
    {
    return t + u;
    }

  • 单纯只有auto

    template <typename T, typename U>
    auto add(T t, U u)
    {
    return t + u;
    }

  • 使用拖尾decltype()decltype(auto),与单纯只有auto的返回类型推断并不等效. 前面介绍了auto会抛弃初始化值类型中的引用
    >.这意味着纯auto返回类型推断有时候会不必要的复制值 可以试试返回 const auto&
  • 参考 https://c.biancheng.net/view/3727.html

四、使用 using 定义类型别名

  • using 覆盖了 typedef 的全部功能

    // 重定义unsigned int
    typedef unsigned int uint_t;
    using uint_t = unsigned int;
    // 重定义std::map
    typedef std::map<std::string, int> map_int_t;
    using map_int_t = std::map<std::string, int>;
    //定义函数类型
    typedef void (*func_t)(int, int);
    using func_t = void (*)(int, int);
    
  • 和模板的结合使用

    template <typename Val>
    using str_map_t = map<string, Val>;
    // ...
    str_map_t<int> map1;
    
  • 别名模板(alias template)

    template <typename T>
    using func_t = void (*)(T, T);
    // 使用 func_t 模板
    func_t<int> xx_2;
    

    using 语法和 typedef 一样,并不会创造新的类型 只是原类型的别名
    func_t 定义的 xx_2 并不是一个由类模板实例化后的类,而是 void(*)(int, int) 的别名。
    从这里可以看出模板非常灵活好用

五、模板

>> [ 1 ]. 模板参数推断

  • 在c++17之前,类模板的参数必须显式指定,而c++17中类模板和函数模板一样,可以从初始化构造函数参数中推断模板参数类型

  • 在c++17之后, 如果类模板所有参数能全部推断出来(包含有默认模板参数的情况) (形参不含引用的情况)则生成对象时不需要指定尖括号<>

  • std::vector v(8,2) //自动推断为vector<int> 类型

  • 在c++17中 允许auto或decltype(auto)用于模板的非类型参数推断

    template<auto N>
    class A {

    };
    A<42> s1; // OK: type of N in A is int
    A<‘a’> s2; // OK: type of N in A is char

  • 不能从函数形参的默认值处(包括构造函数) 推断

     #include <iostream>
     using namespace std;
     template <typename T, typename U>
     void func(T val1 = 0, U val2 = 0)
     {
         //...
     }
     int main()
     {
         func(5,6); //ok
         func();    //编译报错
         return 0;
     }
    
  • 模板参数的推断规则本质上和 auto 一样, 模板类型参数T中是不保留原类型中的引用的,除非你显式指明:func<int&,int&&>

>> [ 2 ]. 默认模板参数

  • #include <iostream>
    using namespace std;
    template <class T=short,class Z=double,class U=char,auto N=5>
    Z func(Z val)
    {
        T t{};
        Z z{};
        U u{};
        decltype(N) n {}; //用decltype()推导右值N的类型
        cout<<N<<endl;
        return val;
    }
    int main()
    {
        //c++17之后允许传递给非类型参数 非全局作用域内此被static修饰的 局部变量的地址
        static int temp{};    
        func(97);                          // T=short, Z=int, U=char, decltype(N)= int
        func<char>(97);                    // T=char, Z=int, U=char, decltype(N)= int
        func<double,long>(97);             // T=double, Z=long, U=char, decltype(N)= int
        func<double,long,short,&temp>(97); // T=double, Z=long, U=short, decltype(N)= int*
        return 0;
        //为模板参数指定默认值时可以是在参数列表的开头, 中间 或 结尾
    }
    

>> [ 3 ]. 模板的非类型参数

  • c++17对模板的非类型参数的规定有所更改(更好用了)
  • 模板的非类型参数可以是以下类型
    >.指针变量必须用constexpr修饰才能传入 要么就用&取变量的地址直接传入 用auto修饰的非类型参数可以自动推断为该变量的指针类型
    • 整型/字符型
    • 对象类型(可以是模板对象)和基本类型的引用或指针
    • 函数(可以是函数模板的实例化函数)的引用或指针
    • 类的成员指针
  • 用法示例:
    #include <iostream>
    using namespace std;
    //类模板
    template <class Z>
    class x
    {
        //....
    };
    //函数模板
    template <class Z>
    void func(Z z){
        //...
    }
    //普通函数
    void func1(char ch){ //
        //...
    }
    //测试非类型参数用 函数模板
    template <class Z, decltype(auto) N>
    Z test(Z val)
    {
        decltype(N) n = N;
        return val;
    }
    //传指针变量时必须用constexpr修饰为常量 表示后面出现pfunc时 可以自动转换为指向的函数的地址常量
    constexpr auto pfunc =func<double>;//必须显示指明,不然不会有这个版本的函数,模板只是个"图纸"
    auto& pfunc1 =func1;  //普通函数的引用 传引用类型时可不用constexpr修饰
    
    constexpr int temp = 5; //基本类型 使其成为编译时常量即可传入模板的非类型参数
    auto & temp1 = temp;    //基本类型的引用
    constexpr auto pTemp = &temp; //基本类型的指针 必须用constexpr声明为常量指针 才能传入
    int main()
    {
        static x<short> obj;   //或者在全局范围声明就不用加static
        static x<short>& obj1 = obj;
    
        decltype(auto) testType = pfunc1;
        //各个版本对非类型参数的要求略有不同 c++98要求必须将temp声明在全局,
        //即使用static修饰也无济于事  直到c++17 (至少gcc下如此)
        test<int,pTemp>(97);      //pTemp为 const int*const 类型     N 为 const int* 类型
        test<int,temp>(97);       //temp为  const int 类型           N 为 int 类型  (把temp当做编译时常量所以没有const)
        test<int,&obj>(97);       //&obj为  obj  对象地址             N 为 x<short>* 类型
        test<int,obj1>(97);       //obj1为  x<short>& 类型           N 为 x<short>& 类型
        test<int,func1>(97);      //func1为 普通函数                  N 为 void(*)(char) 类型
        test<int,func<int>>(97);  //func为  函数模板实例化后的函数       N 为 void(*)(int) 类型
        test<int,pfunc1>(97);     //pfunc1为 普通函数的引用             N 为 void(&)(char) 类型
        test<int,pfunc>(97);      //pfunc为 函数模板实列化后的地址常量 指针  N 为 void(*)(double) 类型
        return 0;
        //函数名会自动转换为地址常量 可以不用 & 取函数的地址
    }
    
  • 在模板内非类型参数是个右值不能修改

>> [ 4 ]. 无名模板参数

  • 如果该模板体不需要用上模板参数则可以不写名字

    template<typename,typename> //可以是一个/可以是多个
    struct test
    {
    	test(){cout<<"i do not need to use this/these type"<<endl;}
    };
    
  • 通常是用在基模板上

    template<typename,typename>
    struct test
    {
        test(){cout<<"i do not need to use this/these type"<<endl;}
    };
    // 特化版本 
    template<typename _Tp,class A>
    struct test<_Tp,A&> //只要有一个参数不一样就能形成特化
    {
        typedef _Tp   type;
        test(){cout<<"Specialization"<<endl;}
    };
    
  • 无名非类型模板参数

    template<auto,auto>
    struct test
    {
        test(){cout<<"i do not need to use this/these non-type parameters"<<endl;}
    };
    // 特化版本
    template<char a,int b>
    struct test<a,b>
    {
        test(){cout<<"Specialization"<<endl;}
    };
    

>> [ 5 ].模板的可变参数

  • 可变参数函数模板
    template<typename... T>
    void vair_fun(T...args) {
       //函数体
    } 
    
    • typename或者class后跟 就表明T是一个可变模板参数,它可以接收多种数据类型,又称模板参数包
    • args 参数的类型用T…表示,表示 args 参数可以接收任意个参数,又称函数参数包
    • vair_fun();可以这样调用,这表明 参数包可以是空包 相当于没有这个参数包
    • 使用可变参数模板的要点在于"解包", 使用包里的数据
      • 递归方式解包
        #include <iostream>
        using namespace std;
        //模板函数递归的出口
        void vir_fun() {
        }
        template <typename T, typename... args>
        void vir_fun(T argc, args... argv)
        {
            cout << argc << endl;
            //开始递归,将第一个参数外的 argv 参数包重新传递给 vir_fun
            vir_fun(argv...);
        }
        int main()
        {
            vir_fun(1, "mytest.variadic_templates", 2.34);
            return 0;
        }
        
      • 借助逗号表达式和初始化列表解包
        #include <iostream>
        using namespace std;
        template <typename T>
        void dispaly(T t) {
            cout << t << endl;
        }
        template <typename... args>
        void vir_fun(args... argv)
        {
            //逗号表达式+初始化列表
            int arr[] = { (dispaly(argv),0)... };
           //{ (display(argv),0)... }会依次展开为  
           //{ (display(1),0), (display("mytest.variadic_templates"),0), (display(2.34),0) }
           //逗号表达式总是返回最后一个值,所以arr数组存储的都是0值
           //arr 数组纯粹是为了将参数包展开,没有发挥其它作用
        }
        int main()
        {
            vir_fun(1, "mytest.variadic_templates", 2.34);
            return 0;
        }
        
        
  • 可变参数类模板
    • 借助逗号表达式和初始化列表解包
      #include <iostream>
      using namespace std;
      template <typename T>
      void dispaly(T t) {
          cout << t << endl;
      }
      template<typename... Tail>
      class demo
      {
      public:
          demo(Tail... vtail) {
              int arr[] = { (dispaly(vtail),0)... };
          }
      };
      int main() {
          demo t(1, 2.34, "mytest.variadic_templates");
          return 0;
      }
      
    • 继承形式基类+构造函数递归解包
      #include <iostream>
      //形式类模板demo 只起一个形式作用 中转作用 可以直接写成声明形式
      template<typename... Values> class demo;//类型必须要用参数包申明符... 不然特化1 会 error!
      //特化1 模板特化/专门化, 因为模板参数包允许空包 继承式递归的出口
      template<> class demo<> {};
      
      //特化2 模板特化(含继承操作)  以继承的方式解包 
      template<typename Head, typename... Tail>
      class demo<Head, Tail...>
              : private demo<Tail...> //继承形式基类
      {
      public:
           //demo<Tail...>(vtail...) 初始化形式基类 , 实际会跳转选择初始化 特化2 
          demo(Head v, Tail... vtail) : m_head(v), demo<Tail...>(vtail...) {
              dis_head();
          }
          void dis_head() { std::cout << m_head << std::endl; }
      protected:
          Head m_head;
      };
      int main() {
          //必须在<>内指明类型 别问 问就是 error
          demo<int, float, std::string> t(1, 2.34, "mytest.variadic_templates"); 
          return 0;
      }
       /*输出:
       	mytest.variadic_templates
      	2.34
      	1
      */
      

六、lambda 函数

  • 基本语法:

    [捕获] (形参列表) mutable noexcept -> 后置返回类型 { 函数体 }

  • 上面mutable noexcept 是可选的 只是表明如果需要用到应该写在哪儿
  • 其中后置返回类型可以省略,编译器会自动推导出 lambda 的返回类型,形参列表也是可选的
    因此最简单的 lambda 定义为 []{}
  • [捕获]用法:
    [捕获]的定义方式
    捕获 功能
    [] 空方括号表示当前 lambda 匿名函数中不导入任何外部变量(全局变量除外);
    [=] 只有一个 = 等号,表示以值传递的方式导入所有外部变量(全局变量除外);
    [&] 只有一个 & 符号,表示以引用传递的方式导入所有外部变量;
    [val1,val2,...] 表示以值传递的方式导入 val1、val2 等指定的外部变量,同时多个变量之间没有先后次序;
    [&val1,&val2,...] 表示以引用传递的方式导入 val1、val2等指定的外部变量,多个变量之间没有前后次序;
    [val,&val2,...] 以上 2 种方式还可以混合使用,变量之间没有前后次序。
    [=,&val1,...] 表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。
    [this] 表示以值传递的方式导入当前的 this 指针,这样就可以无限制直接访问 this 的成员
  • 默认情况下,对于以值传递方式捕获的外部变量,不允许在 lambda 表达式内部修改它们的值
    >.(全局变量除外)
    如果想修改它们,定义时就必须使用 mutable 关键字: [=] mutable {…}
    >.也说了是值传递,所以只能修改的是与外部变量同名的副本变量
  • 从c++14开始 lambda 的形参支持 auto 和 默认实参值
    >.注意:前面讲了auto和函数参数默认值不能用在同一个参数上
  • 考虑:constexpr auto add = [] (auto a,auto b) {return a+b};
    add 是一个值,那么它所属的类型到底是什么呢? 其实 lambda 表达式的结果是一个函数对象
    该函数对象的正式名称是 “lambda闭包”, 但是很多人也称之为 “lambda函数” 或 “lambda”
  • 用法示例:
    #include <iostream>
    #include <algorithm>
    using namespace std;
    int main()
    {
        //display 即为 lambda 匿名函数的函数名
        auto display = [](int a,int b) -> void{cout << a << " " << b;};
        //调用 lambda 函数
        display(10,20);
        
        int num[4] = {4, 2, 3, 1};
        //对 a 数组中的元素进行排序
        sort(num, num+4, [=](int x, int y) -> bool{ return x < y; } );
        for(int n : num){
            cout << n << " ";
        }
        return 0;
    }
    

七、POD and union

  • POD: Plain Old Data 简洁旧数据

  • POD 类型一般具有以下几种特征(包括 class、union 和 struct等)

    1. 没有用户自定义的构造函数、析构函数、拷贝构造函数和移动构造函数。
    2. 不包含虚函数和虚基类。
    3. 非静态成员声明为 public。
  • C++11 允许联合体有静态成员,构造函数,析构函数,重载运算符…像类一样

  • 如果联合体内有一个非 POD 的成员,那么这个联合体的默认构造函数将被编译器删除;其他的特殊成员函数,例如默认拷贝构造函数、拷贝赋值操作符以及析构函数等,也将被删除。

    #include <string>
    using namespace std;
    union U {
       string s;
       int n;
    };
    int main() {
    	U u;   // 构造失败,因为 U 的构造函数被删除
      return 0;
    }
    
  • 解决上面问题的一般需要用到 placement new

    #include <string>
    using namespace std;
    union U {
        string s;
        int n;
    public:
        U() { new(&s) string; }
        ~U() { s.~string(); }
    };
    int main() {
        U u;
        return 0;
    }
    
  • 构造时,采用 placement new 将 s 构造在其地址 &s 上,这里 placement new 的唯一作用只是调用了一下 string 类的构造函数。注意,在析构时还需要调用 string 类的析构函数。

  • placement new 是什么?

    placement new 是 new 关键字的一种进阶用法,既可以在栈(stack)上生成对象,也可以在堆(heap)上生成对象。相对应地,我们把常见的 new 的用法称为 operator new,它只能在 heap 上生成对象。

    • placement new 的语法格式如下:
      new(address) ClassConstruct(…)
    • address 表示已有内存的地址,该内存可以在栈上,也可以在堆上;ClassConstruct(…) 表示调用类的构造函数,如果构造函数没有参数,也可以省略括号。
    • placement new 利用已经申请好的内存来生成对象,它不再为对象分配新的内存,而是将对象数据放在 address 指定的内存中。在本例中,placement new 使用的是 s 的内存空间。

八、range for

  • 基本用法:
    for(T& 变量名: arr){循环体} 变量名表示是arr中每个元素的引用
    for(T 变量名: arr){循环体} 变量名表示是arr中每个元素的复制
  • 示例:

    for (char ch : “HelloC++11-C++17”)
    cout << ch;

  • 只能用于同一类型构成的序列对象
  • range for 不能用于指针

九、右值引用 (rvalue reference)

  • 前文 c++规定 介绍的引用称为 左值引用 (lvalue reference)

  • 右值引用常用于 移动语义完美转发

  • 通常情况下,判断某个表达式是左值还是右值,最常用的有以下 2 种方法。

    • 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。
    • 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
  • 基本用法:
    int && a = 10;

  • a是一个右值引用类型, 但它自己是一个左值,可对 a 取地址

  • 右值引用不能用左值初始化

  • 右值引用可以对右值进行修改

  • 左值(lvalue): 左值 lvalue 是有标识符、可以取地址的表达式

  • 纯右值(prvalue): 纯右值 prvalue 是没有标识符、不可以取地址的表达式

  • 将亡值(xvalue): 表达式static_cast<int&&> (value)的结果可以被右值引用绑定,且具备左值的运行时多态性质,对于这种既有左值的特征,同时又能初始化右值引用的情况, 在c++11中将其归为将亡值
    或长这样:

    int&& f(){
     return 3;
    }
    int main()
    {
     f(); // The expression f() belongs to the xvalue category, because f() return type is an rvalue reference to object type.
     return 0;
    }
    

    或长这样:

    struct As
    {
        int i;
    };
    As&& f(){
        return As();
    }
    int main()
    {
        f().i; // The expression f().i belongs to the xvalue category, because As::i is a non-static data member of non-reference type, and the subexpression f() belongs to the xvlaue category.
        return 0;
    }
    

    在这里插入图片描述

  • 如果一个 prvalue 被绑定到一个引用上,它的生命周期则会延长到跟这个引用变量一样长。
    >.这条生命期延长规则只对 prvalue 有效,而对 xvalue 无效。如果由于某种原因,prvalue 在绑定到引用以前已经变成了 xvalue,那生命期就不会延长。

  • T&& Doesn’t Always Mean “Rvalue Reference”
    -------by Scott Meyers

  • int && var1 = someWidget; // here, “&&” means rvalue reference
    //
    auto&& var2 = var1; // here, “&&” does not mean rvalue reference
    //
    template<typename T>
    void f(std::vector&& param); // here, “&&” means rvalue reference
    //
    template<typename T>
    void f(T&& param); // here, “&&”does not mean rvalue reference

十、万能引用与引用折叠(universal reference and reference collapsing)

  • 万能引用
    • If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a universal reference.
      如果一个变量或者参数被声明为T&&,其中T是被推导的类型,那这个变量或者参数就是一个universal reference。

    • "T需要是一个被推导类型"这个要求限制了universal references的出现范围。
    • 在实践当中,几乎所有的universal references都是函数模板的参数。因为auto声明的变量的类型推导规则本质上和模板是一样的,所以使用auto的时候你也可能得到一个universal references。
    • 和所有的引用一样,你必须对universal references进行初始化,而且正是universal reference的initializer决定了它到底代表的是lvalue reference 还是 rvalue reference:
      • 如果用来初始化universal reference的表达式是一个左值,那么universal reference就变成lvalue reference。
      • 如果用来初始化universal reference的表达式是一个右值,那么universal reference就变成rvalue reference。
    • template<typename T>
      void f(T&& param);
      int a;
      f(a); // 传入左值,那么上述的T&& 就是lvalue reference,(int &)也就是左值引用绑定到了左值
      f(1); // 传入右值,那么上述的T&& 就是rvalue reference,(int &&)也就是右值引用绑定到了右值

    • Universal references只以 T&& 的形式出现!即便是仅仅加一个const限定符都会使得“&&”不再被解释为universal reference:

      template<typename T>
      void f(const T&& param); // “&&” means rvalue reference

    • auto中的universal reference

      int &&a=5;
      auto&& b=a; // a是一个lvalue , auto&& 转化为 int & ; lvalue reference
      auto&& c=std::move(a);// std::move(a)是一个rvalue , auto&& 转化为 int &&; rvalue reference

    • 如果一个表达式的结果是左值, 那么就说这个表达式有左值性(lvalueness)
      如果一个表达式的结果是右值, 那么就说这个表达式有右值性(rvalueness)
    • 值类别(value category)和值类型(value type
      • value category 指的是上面这些左值、右值相关的概念
      • 因为表达式的 lvalueness 或 rvalueness 独立于它的类型,我们就可以有一个 lvalue,但它的类型却是 rvalue reference,也可以有一个 rvalue reference 类型的 rvalue :

        int x = 5;
        decltype(auto) z = std::move(x); // z推导为 int&& , std::move(x)是一个rvalue reference 类型的 rvalue :

  • 引用折叠
    • template<typename T>
      void f(T&& param);
      int x;
      f(10); // invoke f on rvalue
      f(x); // invoke f on lvalue

    • 当用rvalue 10调用 f 的时候, T被推导为 int,实例化的 f 看起来像这样: 这没什么问题

      void f(int&& param); // f instantiated from rvalue

    • 但当我们用lvalue x 来调用 f 的时候,T 被推导为 int&,而实例化的 f 就包含了一个引用的引用:

      void f(int& && param); // initial instantiation of f with lvalue

      • 为了避免编译器对这个代码报错,C++11引入了一个叫做“引用折叠”(reference collapsing)的规则来处理某些像模板实例化这种情况下带来的"引用的引用"的问题。上面折叠为 void f(int& param);
    • 引用折叠只有两条规则:
      • 一个 rvalue reference to an rvalue reference 会变成 (“折叠为”) 一个 rvalue reference.
      • 其他种类的"引用的引用" 会折叠为 lvalue reference.
    • 万能引用 T&& 中 T 最终变成了什么?
      #include <iostream>
      using namespace std;
      template<typename T>
      void f(T&& param){
          decltype(auto) temp{0};
          T T_type = temp;  
      }
      int main() {
          int x;
          int &a=x;
          f(10); // invoke f on rvalue//T_type 为 T 类型 ,  param 为 T&& 类型
          f(x); // invoke f on lvalue //T_type 为 T& 类型  , param 折叠后为 T& 类型
          f(a); // invoke f on lvalue//T_type 为 T& 类型  param 折叠后为 T& 类型
      }
      
      表面上总结:(前面说过模板参数 T 本身的类型推断结果和 auto 类似,不会保留原类型中的引用)
      如果万能引用 T&& 被初始化为 rvalue reference 那么T 最终为 T
      如果万能引用 T&& 被初始化为 lvalue reference 那么T 最终为 T&

十一、移动语义

  • 简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。
  • 在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新copy一份,这大大提高了初始化的执行效率。
  • 示例 :
    #include <iostream>
    using namespace std;
    class demo{
    public:
        demo():num(new int(0)){
            cout<<"construct!"<<endl;
        }
        demo(const demo &d):num(new int(*d.num)){
            cout<<"copy construct!"<<endl;
        }
        //添加移动构造函数
        demo(demo &&d):num(d.num){
            d.num = NULL;
            cout<<"move construct!"<<endl;
        }
        ~demo(){
            cout<<"class destruct!"<<endl;
        }
    private:
        int *num;
    };
    demo get_demo(){
        return demo();
    }
    int main(){
        demo a = get_demo();
        return 0;
    }
    
  • 在 gcc 下添加-fno-elide-constructors 编译标志 执行结果为:

    construct!
    move construct!
    class destruct!
    move construct!
    class destruct!
    class destruct!

  • 当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。

十二、完美转发

  • 指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
  • 举个例子:
    #include <iostream>
    using namespace std;
    //重载被调用函数,查看完美转发的效果
    void otherdef(int & t) {
        cout << "lvalue\n";
    }
    void otherdef(int && t) {
        cout << "rvalue\n";
    }
    //实现完美转发的函数模板
    template <typename T>
    void function(T &&t) {
        otherdef(t);     //t永远是个左值
        otherdef(std::forward<T>(t)); //本例的重点 从类型中保留值的rvalueness和lvaluess
        cout<<"============================"<<endl;
    }
    int main()
    {
        function(5);
        int  x = 1;
        function(x);
        function(move(x));//move(x) 内部调用了 static_cast<int&&>(x)
        int && a = 5;
        function(a);
        return 0;
    }
    
    输出:

    lvalue
    rvalue
    ============================
    lvalue
    lvalue
    ============================
    lvalue
    rvalue
    ============================
    lvalue
    lvalue
    ============================

  • 不难发现,本质问题在于,左值右值在函数调用时,都转化成了左值,使得函数转调用时无法判断左值和右值。
  • std::forward + 万能引用 + 引用折叠 三者结合才能实现完美转发

十三、std::move()与std::forward()源码剖析

  • 先看看 std::remove_reference 是如何工作的

    #include <iostream>
    using namespace std;
    template<typename _Tp>
    struct my_remove_reference
    {
        typedef _Tp   type;
        my_remove_reference(){cout<<"_Tp"<<endl;}
    };
    
    
    // 特化版本
    template<typename _Tp>
    struct my_remove_reference<_Tp&>
    {
        typedef _Tp   type;
        my_remove_reference(){cout<<"_Tp&"<<endl;}
    };
    
    template<typename _Tp>
    struct my_remove_reference<_Tp&&>
    {
        typedef _Tp   type;
        my_remove_reference(){cout<<"_Tp&&"<<endl;}
    };
    
    int main()
    {
    	my_remove_reference<int&&>();
    	my_remove_reference<int>();
    	my_remove_reference<int&>();
        return 0;
    }
    

    输出:

    _Tp&&
    _Tp
    _Tp&

  • std::move

    template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
    
  • std::forward

    • 传入左值时

      template<typename _Tp>
      constexpr _Tp&&
      forward(typename std::remove_reference<_Tp>::type& __t) noexcept
      { return static_cast<_Tp&&>(__t); }
      
    • 传入右值时

      template<typename _Tp>
      constexpr _Tp&&
      forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
      {
       	 static_assert(!std::is_lvalue_reference<_Tp>::value,
      	  "std::forward must not be used to convert an rvalue to an lvalue");
      	  return static_cast<_Tp&&>(__t);
      }
      
    • 传入实参后内部出现了很多万能引用和引用折叠, 总起来说forward做了什么?

      • 对于传入的是个lvalue
        • 如果是lvalue reference类型 则返回他本身类型的 lvalue
        • 如果是rvalue reference类型 则返回他本身类型的 rvalue 或者说 xvalue
        • 如果是不含引用 类型 则和第一种情况一样 返回他本身类型的 lvalue
      • 对于传入的是个rvalue
        • 返回他本身类型的 rvalue 或者说 xvalue

十四、默认构造与禁止构造赋值关键字 与 委托构造函数

  • 默认构造函数

    • 一但自定义了任何构造函数, 编译器不再隐式定义默认的无参构造函数
    • 如果仍然想让对像可被默认构造, 可以使用 default 关键字,不能有参数

      class Box{
      Box() = default;
      };

    • 如果编译器在派生类中提供了无参构造函数,那么其基类中必须有非私有的无参构造函数
  • 禁止构造与赋值
    有时候,你可能想要禁止编译器生成默认的copy构造函数或赋值运算符,可以通过 delete 关键字显式的进行说明:

    class B{
    public:
          B(int){ };
          B(double) = delete;
          B& operator= (const B&) = delete;
          B(const B&) = delete;
    };
    int main() {
          B a(1);
          B a1(3.12); //error 
          B a2(a);  //error 
          a2 = a1; //error 
    }
    

    不会禁止派生类的构造函数, 只是禁止它自己的

  • 委托构造函数

    class B{
    public:
        double length {1.0};
        double width {1.0};
        double height {1.0};
        B(double a,double b,double c):length{a},width{b},height{c} {};
        B(double side):B(side,side,side) {};  //委托构造函数
    };
    

十五、mutable, inline, noexcept, constexpr, if constexpr

  • mutable
    mutable 关键字指出, 即使是 const 对象, 其被 mutable 修饰的成员变量仍可以被修改.
    任何成员函数(包括 const 和非 const 成员函数)总是可以修改用 mutable 声明的成员变量
  • inline
    c++17起开始支持内联变量 使得类的静态成员的声明和定义统一在一起
    (自身类型的静态成员变量必须在类外初始化)
  • noexcept
    • 通过在函数头结尾处追加 noexcept 限定符,可指定该函数不会抛出异常 效果同 throw()
      示例:

      void f() noexcept { //…}

    • 注意,这并不意味在函数体内不能抛出异常, 只是说没有异常能够离开函数(不会传播到调用处)
    • 从c++11开始, 析构函数基本上被编译器隐式声明为 noexcept
      原则上,同过显式添加 noexcept(false) 可以定义能够抛出异常的析构函数 但一般不会这么做
  • constexpr
    constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。
    以前, 初始式是常量表达式的 const 对象称为 编译时常量 否则称为 运行时常量
    #include <iostream>
    #include <array>
    using namespace std;
    void dis_1(const int x){
        //错误,x是运行时常量 但不属于一个常量表达式 所以叫只读属性的变量比较合适
        array <int,x> myarr{1,2,3,4,5};
        cout << myarr[1] << endl;
    }
    void dis_2(){
        const int x = 5;    //编译时常量 属于常量表达式
        array <int,x> myarr{1,2,3,4,5};
        cout << myarr[1] << endl;
    }
    int main()
    {
        dis_1(5);
        dis_2();
    }
    
    • 这是因为,dis_1() 函数中的“const int x”只是想强调 x 是一个只读的变量,其本质仍为变量,无法用来初始化 array 容器;而 dis_2() 函数中的“const int x”,表明 x 是一个只读变量的同时,x 还是一个值为 5 的常量,所以可以用来初始化 array 容器。
    • C++ 11标准中,为了解决 const 关键字的双重语义问题,保留了 const 表示“只读”的语义,而将“常量”的语义划分给了新添加的 constexpr 关键字。因此 C++11 标准中,建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。
    • const 用于为修饰的变量添加“只读”属性;而 constexpr 关键字则用于指明其后是一个常量(或者常量表达式),编译器在编译程序时可以顺带将其结果计算出来,而无需等到程序运行阶段
    • C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。
    • 当常量表达式中包含浮点数时,考虑到程序编译和运行所在的系统环境可能不同,常量表达式在编译阶段和运行阶段计算出的结果精度很可能会受到影响,因此 C++11 标准规定,浮点常量表达式在编译阶段计算的精度要至少等于(或者高于)运行阶段计算出的精度。
    • 注意,获得在编译阶段计算出结果的能力,并不代表 constexpr 修饰的表达式一定会在程序编译阶段被计算出结果,具体的计算时机还是编译器说了算。
    • 使用细节请看 C++中的const, constexpr, consteval, constinit 汇总
  • if constexpr
    if constexpr 是C++17引入的特性, 它会在编译期间对布尔常量表达式评估, 并生成对应分支的代码
    基本语法与普通 if 类似:

    if constexpr (布尔常量表达式) {
    //…分支code
    } else if constexpr (布尔常量表达式) {
    //…分支code
    } else {
    //…分支code
    }


To be continue…


总结

学而不思则罔,思而不学则殆

文章来源:https://blog.csdn.net/Computer_Tech/article/details/135059143
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。