C++新经典 | 记录在最后的高阶知识点

目录

一、函数调用运算符与function类模板

1.函数调用运算符

(1)函数类型

(2)可调用对象

2.function类模板

二、万能引用类型

1.万能引用

2.万能引用资格的剥夺与辨认

(1)const修饰词

(2)类模板成员函数

三、引用折叠

1.编译器合并&的规则

四、std::forward及std::move

1.完美转发std::forward

2.std::move VS std::forward

五、auto类型

1.auto的特点

2.auto类型推断

(1)传值方式(非指针,非引用)

(2)指针或者引用类型但不是万能引用

(3)万能引用类型

3.auto类型std::initializer_list的特殊推断

六、decltype

七、std::function & std::bind

1.详细记录

2.简单说明

八、lambda表达式

1.lambda表达式的特点

2.格式说明

3.捕获列表说明


一、函数调用运算符与function类模板

1.函数调用运算符

        函数调用总离不开一对圆括号,“()”就是函数调用的一个明显标记,这个“()”有一个称呼叫函数调用运算符。

int Add(int value)
{
    return value + 1;
}

        如果在类中重载了这个函数调用运算符“()”,就可以像使用函数一样使用该类的对象,或者换句话说就可以像函数调用一样来“调用”该类的对象。写出来这个函数调用运算符后,怎样使用呢?分两步使用即可:

(1)定义一个该类的对象。

(2)像函数调用一样使用该对象,也就是在“()”中增加实参列表。

class TestOperator {
public:
    int operator()(int value) const    //重载函数调用运算符
    {
        return value + 1;
    }
};
void main()
{
    TestOperator test;
    std::cout << test(5) << std::endl; //6
}

        Add函数以及TestOperator类中重载的函数调用运算符,它们的形参和返回值是相同的,这就叫作“调用形式相同” 。

        一种调用形式对应一个函数类型,所以因为Add函数以及TestOperator类中重载的函数调用运算符调用形式相同,所以它们函数类型相同。

(1)函数类型

int(int)

        上面这行代表一个“函数类型”:接收一个int参数,返回一个int值。

(2)可调用对象

        “可调用对象”这个概念(“函数对象”“仿函数”都是同一个意思),那么如下两个都是可调用对象:

  • Add函数。
  • 重载了函数调用运算符“()”的TestOperator类所生成的对象。

2.function类模板

        C++11中,标准库里有一个叫作function的类模板,这个类模板是用来包装一个可调用对象的。要使用这个类模板,当然是要提供相应的模板参数,这个模板参数就是指该function类型能够表示(包装)的可调用对象的调用形式:

function

        上面这种写法是一个类类型(类名就是function),用来代表(包装)一个可调用对象,它所代表的这个可调用对象接收一个int参数,返回一个int值。

        如果上面的Add函数有一个重载,那么就无法包装进function中。这也是一个二义性导致的问题,那么这个问题怎样解决?可以通过定义一个函数指针来解决,因为函数指针中含有参数类型和返回类型。

int Add(int value)
{
    return value + 1;
}
int Add(int value1,int value2)
{
    return value1 + value2;
}
void main()
{
    int(*p)(int)= Add;                    //定义一个函数指针p,不会产生二义性。因为函数指针中含有参数类型和返回类型。
    std::function fun = p;
    std::cout << fun(5) << std::endl;     //6

    int(*p1)(int,int)= Add;    
    std::function fun1 = p1;
    std::cout << fun1(5,6) << std::endl;  //11
}

二、万能引用类型

        universal reference(后来被称为forwarding reference:转发引用)翻译成中文有好几种翻译方法,取两种最常见的翻译,一种叫“万能引用”,一种叫“未定义引用”,都是一个意思,后续就称为“万能引用”了。

1.万能引用

        万能引用是一种类型。就跟int是一种类型一个道理,再次强调,万能引用是一种类型。

        万能引用(又名未定义引用,英文名universal reference)离不开两种语境,这两种语境必须同时存在:

  • 必须是函数模板。
  • 必须是发生了模板类型推断并且函数模板形参长这样:T&&。
void Test(int&& data)  //形参是一个右值引用,实参需要一个右值
{
    std::cout << data << std::endl;
}
void main()
{
    int i = 10;
    Test(i);//错误,i是左值,而参数需要一个右值
}
template 
void Test(T&& data) //T&&万能引用,参数既可以支持左值,也可以是右值
{
    std::cout << data << std::endl;
}
void main()
{
    int i = 10;
    Test(i);        //正确
}

        万能引用就长这样:T&&(T必须与&&挨着)。它也用两个地址符号“&&”表示,所以万能引用长得跟右值引用一模一样。但是解释起来却不一样(注意语境,只有在语境满足的条件下,才能把“&&”往万能引用而不是往右值引用解释):

(1)右值引用作为函数形参时,实参必须传递右值进去,不然编译器报错,上面已经看到了。

(2)而万能引用作为函数形参时,实参可以传递左值进去,也可以传递右值进去。所以,万能引用也被人叫作未定义引用。如果传递左值进去,那么这个万能引用就是一个左值引用;如果传递右值进去,那么这个万能引用就是一个右值引用。从这个角度来讲,万能引用更厉害:是一种中性的引用,可以摇身一变,变成左值引用,也可以摇身一变,变成右值引用。

T&&才是万能引用,千万不要理解成T是万能引用,其实,data的类型是T&&,所以data的类型才是万能引用类型(或者理解成data才是万能引用),这意味着如果传递一个整型左值进去,data的类型最终就应该被推断成int&类型;如果传递一个整型右值进去,data最终就应该被推断成int&&类型。

 下面的两种情况才是万能引用,其他的看到的“&&”的情况都是右值引用:

  • 一个是函数模板中用作函数参数的类型推断(参数中要涉及类型推断),形如T&&。
  • auto&& tmpvalue=…也是一个万能引用。

2.万能引用资格的剥夺与辨认

(1)const修饰词

        const修饰词会剥夺一个引用成为万能引用的资格,被打回原形成右值引用。T&&前后左右都不要加什么修饰符,不然很可能就不是万能引用而直接退化为右值引用了。

template 
void Test1(const T&& data) //const修饰词会剥夺一个引用成为万能引用的资格,被打回原形成为右值引用。
{
    std::cout << data << std::endl;
}
void main()
{
    int i = 10;
    Test1(i);//错误,i是左值,而参数需要一个右值
}

(2)类模板成员函数

        如下方代码Test3后面的T&&不是一个万能引用,而是一个右值引用。因为Test3成员函数本身没有涉及类型推断,Test3成员函数是类模板TEST的一部分。

template 
class TEST {
public:
    void Test3(T&& data)//T&&不是万能引用,实参需要一个右值
    {
        std::cout << data << std::endl;
    }
};

        如果搞不清楚形参类型是否是一个万能引用,则分别传递进去一个左值和一个右值作为实参来调用,就可以验证。


三、引用折叠

        引用折叠是一个C++11新标准里出现的概念,同时也是一条规则,英文名字是reference-collapsing rules(引用折叠规则),有人也翻译成引用坍塌,都是一个意思。想一想,“折叠”这个词,从字面分析,就是给鼓捣没一些东西或者折起来一些东西,这里三个&就给鼓捣剩1个了,这就是引用折叠规则的体现。

        在C++中,有明确含义的“引用”只有两种,一种是带一个&的左值引用,一种是带两个&&的右值引用。但是,此时此刻竟然出来三个&(&&&),并且这三个&应该是第一个一组,后两个一组,也就是& &&这种感觉。

        第一组是一个左值引用,第二组因为已经实例化过了,所以第二组的两个&&就不可能再是万能引用了,所以第二组的两个&&实际是右值引用。现在编译器遇到超过两个&了,第一组一个&,第二组两个&&,编译器就会把超出两个的&合并,实际上合并的原因不仅仅是因为超过两个&,只要是左值之间相遇、左值和右值相遇、右值之间相遇,以及右值和左值相遇,系统都会进行&符号的合并,合并后的&数量肯定比合并之前少,所以才叫“折叠”嘛!

        写程序的时候不能写出三个&&&,否则是语法错误。

template 
void Test(T&& data) //T&&万能引用,参数既可以支持左值,也可以是右值
{
    std::cout << data << std::endl;
}
void main()
{
    int a = 10;
    int& b = a;
    Test(b);
}

C++新经典 | 记录在最后的高阶知识点_第1张图片

1.编译器合并&的规则

        左值引用是一个&表示,右值引用是两个&&表示,如果任意一个引用为左值引用,结果就为左值引用(左值引用会传染),否则结果为右值引用。所以这两种引用组合一下就会有四种可能的组合:

  • 左值&-左值&:左值引用&
  • 左值&-右值&&(这是上面范例当前的情形):左值引用&
  • 右值&&-左值&:左值引用&
  • 右值&&-右值&&:右值引用&& 

四、std::forward及std::move

        注意:形参总是左值,即使其类型是右值引用。如下代码:num是一个左值,但它的类型是右值引用。

void Function(int&& num)
{
}

        对于一个变量: 

  • 如果从左值还是右值这个概念/角度来讲,它要么是左值,要么是右值,不可能是其他东西。当这个变量作为实参传递给某个函数的形参时,如果这个变量是左值,那么这个函数的形参必须得是一个左值引用;如果这个变量是右值,那么这个函数的形参必须得是一个右值引用。否则就会报语法错。
  • 如果从类型这个概念/角度来讲,它要么是一个左值引用,要么是一个右值引用(万能引用不算,因为万能引用只出现在类型推导中,不在讨论范围之内)。它到底是左值引用还是右值引用,看它是怎样定义的。如果它这样定义:“int &ta …;”,显然,ta是一个左值引用,如果它这样定义:“int &&ta…;”,显然,ta是一个右值引用。所以左值引用只能绑到一个左值上去(例如左值引用作函数形参时,实参只能是左值),右值引用只能绑到一个右值上去(例如右值引用作函数形参时,实参只能是右值)。
template 
void Test(F fun,T1&& data1,T2&& data2)    //data1和data2都是万能引用类型,那么从实参中来的左值或者右值信息、const信息就都会保存在data1和data2参数中
{                                         
    //fun(data1, data2);//报错  无法将参数 1 从“T1”转换为“int &&” ,因为data1和data2都是左值,而Function的第一个参数需要一个右值

    fun(std::forward(data1), std::forward(data2));
}

void Function(int&& num1, int& num2) //第一个参数需要右值,第二个参数需要左值;num1的类型是右值引用,但num1本身是左值 ;右值引用作函数形参时,实参只能是右值
{
    std::cout << num1<<","<

        有个实参是一个值(10),传递到一个函数模板(Test)中之后,这个模板中有个形参(data1),专门接这个右值,所以这个形参是一个右值引用,但是这个形参本身是一个左值。试想,原来是一个右值,接收到后变成了左值,那如果另外一个函数(Function的num1参数)正好需要一个右值绑定过来,那就得把这个左值再变回右值去,这就用到std::forward函数。

        注意:万能引用中,给一个int类型的左值,T推导出来的类型为int &;给一个int类型的右值,T推导出来的类型为int。即:T1是int类型,T2是int&类型。当T推导为int &类型时还发生了引用折叠。

1.完美转发std::forward

        完美转发比较好地解决了参数转发的问题,通过一个函数模板,可以把任意的函数名、任意类型参数(只要参数个数是确定的)传递给这个函数模板,从而达到间接调用任意函数的目的。std::forward的能力就是保持原始实参的左值或者右值性。

        对这个函数有两种理解:

  • 实参原来是一个左值(j),到了形参中还是左值(data2)。forward能够转化回原来该实参的左值或者右值性,所以std::forward之后还是一个左值。
  • 实参原来是一个右值(10),到了形参中变成了左值(data1)。forward能够转化回原来该实参的左值或者右值性,所以std::forward之后还是一个右值。

        所以,从现在的情形看,forward有强制把左值转成右值的能力。所以,看起来std::forward这个函数只对原来是一个右值这种情况有用。

        回顾上述代码:“Test(Function,10, num2);”,这里的10本来是一个右值,到了void Test(F fun,T1&& data1,T2&& data2)里面去之后,向Funciton转发的时候“fun(data1, data2);”中的data1变成了一个左值,是std::forward使它恢复了右值身份:std::forward(data1),所以这里可以把forward看成是强制把一个左值转换成了一个右值。

        std::forward就是通过“<>”里面的类型决定把变量转成左值还是右值,当forward的尖括号里是int时,表示转成右值;当forward的尖括号里是int&,表示转成左值。 

2.std::move VS std::forward

  • std::move是属于无条件强制转换成右值(如果原来是左值,就强制转成右值;如果原来是右值,那就最好,保持右值不变)。std::forward是某种条件下执行强制转换。例如原来是右值,因为某些原因(如参数传递)被折腾成左值了,此时forward能转换回原来的类型(右值);如果原来是左值,那forward就什么也不干。
  • std::forward总感觉用起来方便性不够,因为既得为其提供一个模板类型参数,又要提供一个普通参数。而std::move只需要一个普通参数,不需要模板类型参数。
  • 从具体书写代码的角度谈:
    • std::move后面往往是接一个左值,std::move是把这个左值转成右值,转成右值的目的通常都是做对象移动(所以这个对象应该支持移动,前面讲过移动构造函数)的操作。不然平白无故地转成右值也没多大意义。
    • std::forward往往是用于参数转发,所以不难发现,std::forward后面圆括号中的内容往往都是一个万能引用(如std::forward(t)中的t就是一个万能引用)。

五、auto类型

1.auto的特点

  • auto的自动类型推断发生在编译期间。
  • auto定义变量必须立即初始化,这样编译器才能推断出它的实际类型。编译的时候才能确定auto的类型和整个变量的类型,然后在编译期间就可以用真正的类型替换掉auto这个类型占位符。
  • auto的使用比较灵活,可以和指针、引用、const等限定符结合使用。
  • auto不能用于函数参数,如void myfunc(auto x,inty)不可以。
  • 非静态数据成员不能具有包含auto的类型。静态数据成员可以使用auto。

2.auto类型推断

(1)传值方式(非指针,非引用)

        传值方式针对auto类型:会抛弃引用、const等限定符。

    auto num = 10;          //num为int类型
    const auto num1 = num;  //num1为const int类型
    auto num2 = num1;       //num2为int类型,会抛弃const限定符

    const auto& num3 = num; //不属于传值方式。num3为const int&类型
    auto num4 = num3;       //属于传值方式。num4为int类型,会抛弃引用、const等限定符

(2)指针或者引用类型但不是万能引用

        指针或者引用方式针对auto类型:不会抛弃const限定符,但是会抛弃引用。

    auto num = 10;           //num为int类型
    auto &num1 = num;        //num1为int&类型
    const auto* num2 = # //num2为const int*类型
    auto num3 = num2;        //num3为const int*类型
    auto* num4 = &num1;      //num3为int*类型,会抛弃引用限定符

(3)万能引用类型

        和万能引用模板推理类似:给一个int类型的左值,auto推导出来的类型为int &;给一个int类型的右值,auto推导出来的类型为int。

3.auto类型std::initializer_list的特殊推断

        std::initializer_list也是C++11引入的一个新类型(类模板),表示某种特定类型的值的数组。当用auto定义变量并初始化时如果用“={}”括起来,则推导出来的类型就是std::initializer_list了。

        std::initializer_list其实是一个类模板,它包含一个类型模板参数用来表示其中的成员类型(也就是这个数组里面所保存的成员类型),这个成员的类型也需要推导(这是第二次推导),如果“{}”中的成员类型不一致,则成员类型的推导会失败。

    auto num = 10;                  //num为int类型
    auto num1 = { 10 };             //num1为std::initializer_list类型
    auto num2 = { 10,20,30 };       //num1为std::initializer_list类型
    //auto num3 = { 10,20,30.1f };  //错误,无法推导auto类型

        对象的初始化方式五花八门,有用“()”的,有用“{}”的,也有用“={}”的等。为了统一对象的初始化方式,在C++11中,引入了“统一初始化”的概念,英文名就是Uniform Initialization,试图努力创造一个统一的对象初始化方式 。

        当编译器看到这种大括号括起来的形如{"aa","bb","cc"}的内容,一般就会将其转化成一个std::initializer_list。所以可以这样认为,所谓的“统一初始化”,其背后就是std::initializer_list进行支持的。

六、decltype

        decltype和auto有类似之处,两者都是用来推断类型的。decltype有如下特点:

  • decltype的自动类型推断也发生在编译期,这一点和auto一样。
  • decltype不会真正计算表达式的值。
  • const限定符、引用属性等有可能会被auto抛弃,但decltype一般不会抛弃任何东西。

        固定规则:①decltype后面是一个非变量的表达式(*p);②且该表达式能够作为等号左边的内容(*p=20;);那么decltype得到的类型就是一个形如“类型名&”的左值引用类型:

    auto num = 10;
    int *p = #
    *p = 20;
    decltype(*p) num1 = num;     //num1为 int& 类型

        decltype((变量))的结果永远是引用。注意这里是双层括号。而decltype(变量),除非变量本身是引用,否则整个decltype的类型不会是引用。

decltype后的“()”里本来是一个变量,但如果这个变量名上额外加了一层括号或者多层括号,那编译器就会把这个变量当成一个表达式,又因为变量名可以放在等号左侧(左值),所以会推断出引用类型。

    auto num = 10;
    int temp = 30;
    decltype(num) num1 = 20;         //num1为 int 类型
    decltype((num)) num2 = temp;     //num2为 int& 类型
    int temp = 30;
    auto &num = temp;                //num为 int& 类型
    decltype(num) num1=temp;         //num1为 int& 类型

七、std::function & std::bind

1.详细记录

boost | 函数与回调(一)ref与bind_boost::bind ref-CSDN博客

【精选】boost | 函数与回调(二)function_function回调函数_烫青菜的博客-CSDN博客

2.简单说明

        std::bind能将对象以及相关的参数绑定到一起,绑定完后可以直接调用,也可以用std::function进行保存,在需要的时候调用。

        std::bind有两个意思:

  • 将可调用对象和参数绑定到一起,构成一个仿函数,所以可以直接调用。
  • 如果函数有多个参数,可以绑定部分参数,其他的参数在调用的时候指定。 

        因为有了占位符(placeholder)这种概念,所以std::bind的使用就变得非常灵活。可以直接绑定函数的所有参数,也可以仅绑定部分参数。

        绑定部分参数时,就需要通过std::placeholders来决定bind所在位置的参数将会属于调用发生时的第几个参数。

  • std::bind的思想实际上是一种延迟计算的思想,将可调用对象保存起来,然后在需要的时候再调用。
  • std::function一般要绑定一个可调用对象,类成员函数不能被绑定。而std::bind更加强大,成员函数、成员变量等都能绑定。现在通过std::function和std::bind的配合,所有的可调用对象都有了统一的操作方法。

八、lambda表达式

1.lambda表达式的特点

    [捕获列表](参数列表)->返回类型 {
        函数体;
    };
  • 它是一个匿名函数,也可以理解为可调用的代码单元或者是未命名的内联函数。
  • 它也有一个返回类型、一个参数列表、一个函数体。
  • 与函数不同的是,lambda表达式可以在函数内部定义(上面范例就是在main主函数中定义的lambda表达式),这个是常规函数做不到的。

2.格式说明

  • 返回类型是后置的这种语法(lambda表达式的返回类型必须后置,这是语法规定)。因为很多时候lambda表达式返回值非常明显,所以允许省略lambda表达式返回类型定义——“->返回类型”都省略了,编译器能够根据return语句自动推导出返回值类型。
  • 没有参数的时候,参数列表可以省略,甚至“()”也可以省略。
  • 捕获列表[]和函数体不能省略,必须时刻包含。
  • lambda表达式的调用方法和普通函数相同,都是使用“()”这种函数调用运算符。
  • lambda表达式可以不返回任何类型,不返回任何类型就是返回void。
  • 函数体末尾的分号不能省。

3.捕获列表说明

        lambda表达式通过捕获列表来捕获一定范围内的变量。捕获这个概念,只针对在创建lambda表达式的作用域内可见的非静态局部变量(包括形参)。即:捕获是不包括静态局部变量的,也就是说,静态局部变量不能被捕获,但是可以在lambda表达式中使用。另外,静态局部变量保存在静态存储区,它的有效期一直到程序结束。

  • []:不捕获任何变量。
  • [&]:捕获外部作用域中所有变量,并作为引用在函数体内使用。
  • [=]:捕获外部作用域中所有变量,并作为副本(按值)在函数中使用,也就是可以用它的值,但不能给它赋值。
  • [this]:一般用于类中,捕获当前类中this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了“&”或者“=”,则默认添加了此项(this项)。也就是说,捕获this的目的就是在lambda表达式中使用当前类的成员函数和成员变量。注意,针对成员变量,[this]或者[=]可以读取,但不可以修改。如果想修改,可以使用[&]。
  • 按值捕获和按引用捕获。
    • [变量名]:按值捕获(不能修改)变量名所代表的变量,同时不捕获其他变量。
    • [&变量名]:按引用捕获(可以修改)变量名代表的变量,同时不捕获其他变量。
    • 上面的变量名如果是多个,之间可以用逗号分隔。当然对于按引用捕获,多个变量名的情况下,每个变量名之前也都要带有&。
  • [=,&变量名]:按值捕获所有外部变量,但按引用捕获“&”中所指的变量(如果有多个要按引用捕获的变量,那么每个变量前都要增加“&”),这里“=”必须写在开头位置,开头这个位置表示“默认捕获方式”。也就是说,这个捕获列表第一个位置表示的是默认捕获方式(也叫隐式捕获方式),后续其他的都是显式捕获方式。
  • [&,变量名]:按引用捕获所有外部变量,但按值捕获变量名所代表的变量。这里这个“&”必须写在开头位置,开头这个位置表示“默认捕获方式”。
void main(){
   auto function= [](int a)->int{   //lambda表达式创建的匿名函数
       return ++a;
   };
   //或者写成:
   std::function function= [](int a)->int{  
       return ++a;
   };
   //或者写成:
   int(*function)(int)=[](int a)->int{  
       return ++a;
   };
   std::cout << function(10) << std::endl;
}

你可能感兴趣的:(#,C++新经典,c++)