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

目录

  • 前言
  • 一、{列表初始化}
  • 二、auto 与 decltype 与 decltype(auto)
  • 三、拖尾返回类型 (trailing-return-type)
  • 四、使用 using 定义类型别名
  • 五、模板
  • 六、lambda 函数
  • 七、POD and union
  • 八、range for
  • 九、右值引用 (rvalue reference)
  • 十、万能引用与引用折叠(universal reference and reference collapsing)
  • 十一、移动语义 与 引用限定符
  • 十二、完美转发
  • 十三、std::move()与std::forward()源码剖析
  • 十四、默认构造与禁止构造赋值关键字 与 委托构造函数
  • 十五、mutable, inline, noexcept, constexpr, if constexpr
  • 十六、重载继承函数 和 用override,final管理虚函数
  • 十七、作用域内枚举
  • To be continue…
  • 总结


前言

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

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

一、{列表初始化}

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

  • int b{} 初始化为0

  • #include 
    
    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 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 list)" << std::endl;
            }
        };
        // 非聚合体首先考虑 initializer_list 构造函数
        Demo d{1, 2}; //花括号里的数据的类型必须一致
        Demo1 d1({1, 2}); //等同于d1(1,2) d1{1,2} 编译时未发现initializer_list形参 就把括号内的值一一匹配赋给 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 list)
        Demo1(int a, int b)
        Demo2(std::initializer_list list) 
          需要注意的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。
       并且,拷贝或赋值一个initializer_list对象不会拷贝列表中的元素,其实只是引用而已,原始列表和副本共享元素。
       和使用vector一样,我们也可以使用迭代器访问initializer_list里的元素
       如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内:*/
    }
    
  • 与数组的关系

    #include 
    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
    auto add(T t, U u) -> decltype(t + u)
    {
    return t + u;
    }

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

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

  • 单纯只有auto

    template
    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) 的别名。
    从这里可以看出模板非常灵活好用

五、模板

  • 模板的篇幅过长所以另开一篇 << C++ Template >>

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

    #include 
    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 (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;
    }
    

    C++11-C++17新特性介绍_第1张图片

  • 如果一个 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
    void f(std::vector&& param); // here, “&&” means rvalue reference
    //
    template
    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
      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
      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
      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 
      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 
      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!

    • 当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
    • C++11只是提供了能构成移动语义的条件 &&, 具体要怎么实现你的移动语义是按你的项目来定的(是你自己的事)

  • 引用限定符
    • 某些场景中,我们可能需要限制调用成员函数的对象的类别(左值还是右值),为此 C++11 新添加了引用限定符。所谓引用限定符,就是在成员函数的后面添加 “&” 或者 “&&”
    #include 
    using namespace std;
    class demo {
    public:
        demo(int num):num(num){}
        int get_num()&&{
            return this->num;
        }
    private:
        int num;
    };
    int main() {
        demo a(10);
        //cout << a.get_num() << endl;      // 错误
        cout << move(a).get_num() << endl;  // 正确
        return 0;
    }
    

十二、完美转发

  • 指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
  • 举个例子:
    #include 
    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(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 
    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
        • 如果是不含引用 类型 返回(他本身类型+&&(rvalue reference) )类型的 rvalue
          逆推: 万能引用下 T 变成不含引用的情况, 那么传入的参数必定是个右值. 也就是说 forward 只在万能引用领域里才像那么回事
      • 对于传入的是个rvalue
        • 返回他本身类型的 rvalue

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

  • 默认构造函数

    • 一但自定义了任何构造函数, 编译器不再隐式定义默认的无参构造函数
    • 如果仍然想让对像可被默认构造, 可以使用 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 
    #include 
    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
    }

十六、重载继承函数 和 用override,final管理虚函数

  • 重载继承函数
    • 通常继承时,产生名字遮蔽的函数,派生类中需要用上域解析符基类名::产生名字遮蔽的函数才能调用基类的, 默认是不构成重载的.
    • 而用上using 就可以在派生类中重载该产生名字遮蔽的函数
      • 如果在public作用域下还能让派生类生成的对象调用该重载
    #include 
    #include 
    using namespace std;
    
    class A{
    public:
        void f(){cout<<"A f"<<endl;}
        void f1(char){cout<<"A f1"<<endl;}
        void f3(int){cout<<"A f3"<<endl;}
    private:
    
    };
    class B:public A{
    public:
        using A::f;
        using A::f1;
        //原型同基类中的 f() 相同
        void f(){ cout<<"B f"<<endl;}
        //原型同基类中的 f1(char) 不同 构成重载
        void f1(){
            f();
            cout<<"B f1"<<endl;
        }
    private:
    
    };
    int main()
    {
       A a;
       B b;
       b.f3(5);   //不产生名字遮蔽的函数 
       b.f1('a'); //调用基类中的 f1(char)
       cout<<"----------"<<endl;
       b.f1();  //调用派生类中的 f1()
    }
    
  • override final管理虚函数
    • void f() override {…}
      override 说明符说明该函数是对其基类原型一致的虚函数的重定义 主打一个标识作用避免混乱
    • virtual void f() final {…}
      final 限定符限定了这就是最后一个虚函数 其派生类不能再重定义该虚函数void f();

十七、作用域内枚举

  • 传统的枚举存在一些问题
    enum egg {Small,Medium,Large,Jumbo}
    enum t_shirt {Small,Medium,Large,Jumbo}
  • egg Small和t_shirt Small位于相同的作用域内,他们将发生冲突,为避免这种问题,C++11提供了一种新枚举
    其枚举的作用域为类:
    enum class egg {Small,Medium,Large,Jumbo}
    enum class t_shirt {Small,Medium,Large,Jumbo}
  • 也可使用struct代替class. 无论用哪种方式, 都需要使用枚举名来限定枚举量:
    egg choice = egg::Large
    t_shirt Floyd = t_shirt::Large
  • C++11 还提高了作用域内枚举的类型安全. 在有些情况下, 常规枚举将自动转换为整型, 但作用域内枚举不能隐式地转换为整型

To be continue…


总结

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

你可能感兴趣的:(C/C++,c++,windows,开发语言)