【c++】第八章 未归类知识点

本文深入探讨C++中的函数调用运算符、万能引用、类型推断、可调用对象等高级主题,解析std::function、std::bind的用法,并介绍lambda表达式的强大功能,以及萃取技术在泛型编程中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第1节  函数调用运算符、function类模板

一、学习c++体会、总述

1、对语言本身的学习  2、大量练习  3、阅读别人的优秀代码

函数调用标记():函数调用运算符

class biggerthanzero {
public:
    int operator()(int value) const {
        if (value < 0)
            return 0;
        return value;
    }
};
//调用
biggerthanzero obj;
cout<<obj(5)<<endl;    //等价于obj.operator()(5)

二、不同调用对象的相同调用形式

若有一个普通函数的参数与返回值与重载的()的参数与返回值相同,这个两个对象的调用形式就是相同的,可以用funciton存储。

三、标准库function类型介绍 #include<functional>

//function<int(int)> 声明一个可调用对象,该可调用对象的参数为int,返回为int
function<int(int)> f1 = echovalue;
function<int(int)> f2 = obj;
function<int(int)> f3 = biggerthanzero();
f1(5);
cout<<f2(-5)<<endl;
cout<<f3(10)<<endl;

简单方法:

map<string, function<int(int)>> m2 = {
    { "ev",echovalue },
    { "obj",obj },
    { "btz",biggerthanzero() }
};
m2["ev"](55);
cout << m2["obj"](-5) << endl;
cout << m2["btz"](10) << endl;

【注】若echovalue有其他参数的函数重载,该可调用对象就不能放入function中,可以定义一个函数指针来解决二义性。

 

第2节  万能引用universal reference

一、类型区别基本概念

二、万能引用(未定义引用、universal reference)

前提:a)必须是函数模板 b)必须发生了模板类型推断 c)形参类型必须为T&&

效果:若传入左值,万能引用变为左值(int&),若传入右值,万能引用变为右值(int&&)

template<typename T>
    void myfun(T &&tmp) {
        cout << tmp << endl;
}
//调用
int i = 100;
myfun(i);
myfun(100);

三、万能引用资格的剥夺与辨认

1、剥夺:const T&& tmp 打回原形,直接变成右值引用

2、辨认:类模板中的成员函数形参中的T&& tmp是右值引用(这是因为类对象声明时类模板的类型推断已经进行,对于成员函数testfunc来说相当于void testfunc(int &&){},但是,类模板中的模板成员函数可以是一个万能引用)。

 

第3节  理解模板类型推断、查看类型推断结果

一、如何查看类型推断结果:依赖boost库打印推断信息

template<typename T>
void myfunc(T tmp) {
    cout << "--------------begin--------------" << endl;
    using boost::typeindex::type_id_with_cvr;
    cout << "T = " << type_id_with_cvr<T>().pretty_name() << endl;
    cout << "tmp = " << type_id_with_cvr<decltype(tmp)>().pretty_name() << endl;
    cout << "---------------end---------------" << endl;
}

二、理解模板类型推断

1、指针或引用类型(不是万能引用)

对于引用:void myfunc(T& tmp)

【形参不加const】1)实参是引用类型,引用会被忽略,T不会被推倒为引用类型

                               2)实参中带const,形参会成为const&

【形参加上const】1)同上 2)实参中有const,会抵消,T就不带const了

对于指针:void myfunc(T* tmp)

若形参中不带const,实参中的const会传入T;若形参中带const,T的推断都不带const(带就重复了)。

2、万能引用:void myfunc(T&& tmp)

int i = 18;    i是左值,T=int&,tmp=int&
const int j = i;    j是左值,T=const int&,tmp=const int&
const int &k = i;    k是左值,T=const int&,tmp=const int&
myfunc(100);    i是右值,T=int,tmp=int&&

3、传值方式:void myfunc(T tmp)

const穿不进去,因为tmp是一个新副本,但是可以传指针进去,如下:

char mystr[] = "dsfaf";
const char* const point = mystr;
myfunc(point);

T = const char*,tmp = const char*,这证明只传了前一个const,即数组内元素不可变但指向可以变。

4、数组作实参

char mystr[] = "dsfaf";
myfunc(mystr);

T = char*,tmp=char*

5、函数名作实参

void myfunc(T tmp){
    推断结果:T=void(*)(void),tmp=void(*)(void),
}
void myfunc(T &tmp){
    推断结果:T=void(void),tmp=void(&)(void),
}

三、总结

1、引用类型的实参等于不存在,推断完也是不带引用类型的

2、万能引用实参为左值还是右值推断的结果是不同的

3、按值传递的const无效,指针另当别论

4、数组、函数名作实参被看做是指针,除非函数模板的形参是引用

 

第4节  引用折叠、转发、完美转发、forward

一、引用折叠规则 c++11

int i = 18;
myfunc(i);
i是左值,T推断为int&,但是tmp应该推断为T&&&啊?

c++有明确含义的引用只有左值引用和右值引用

四中情况的出现会出现引用折叠:第一组(左或右)+第二组(左或右)

【规则】只要有左值引用,结果就为左值,否则为右值!

引用的引用是非法的:& &这种格式一出戏就会报错

二、转发、完美转发

转发:函数模板把收到的参数调用函数来转给该函数(要保证类型不变)

template<typename F, typename T1, typename T2>
void myFuncTemp(F f, T1 t1, T2 t2) {	//f就是要转发到的目标函数
    f(t1,t2);
}
void myfunc(int i, int j) {	//若这里加引用,无法传到外界,改变的其实是t2
    j++;
    cout << i + j << endl;
}
void func() {
    int i = 10;
    myFuncTemp(myfunc, 5, i);
}

目的:修改myFuncTemp模板函数,让这个模板函数的参数能够保持给定实参的左值性和const属性?

万能引用出场!!!

void myFuncTemp(F f, T1 && t1, T2 && t2)    //若为T&,则只能传const属性

又发现问题:

template<typename F, typename T1, typename T2>
void myFuncTemp(F f, T1 && t1, T2 && t2) {
    f(t1,t2);
}
void myfunc(int &&i, int &j) {
    j++;
    cout << i + j << endl;
}
void func() {
    int a = 10;
    myFuncTemp(myfunc, 5, a);
}

这样虽然a的值会变,但是当往函数模板myFuncTemp中传入一个右值时,经过f(t1,t2)的中转,t1变成了左值,使得myfunc中的第一个参数无法接收,程序报错!

【完美转发】接受任意类型实参的函数模板

三、std::forward

前提:模板函数+万能引用+转发

能力:按照参数本来的类型转发,强制左值转右值(如果传进来的就是左值,则不动)

f(std::forward<T1>(t1),std::forward<T2>(t2));    //T1中存着原来的实参类型

四、std::move和std::forward区别

forward是有条件的(如上),move不管什么都转成右值,而且move完了就不用了。

void printInfo(int &tmp) {
    cout << "left" << endl;
}
void printInfo(int &&tmp) {
    cout << "right" << endl;
}
template<typename T>
    void myFuncTemp(T&& t) {
    printInfo(t);
    printInfo(std::forward<T>(t));
    printInfo(std::move(t));
}
void func() {
    int i = 1;
    myFuncTemp(100);    //left/right/right
    myFuncTemp(i);    //left/left/right
}

五、再谈万能个引用

并不是一种新的引用类型,只是一种写程序的方式。

 

第5节 理解auto类型推断、auto应用场合

一、std::forward用法补充:单纯地转为右值

int d = 1;
int &&i = forward<int>(d);

二、auto类型常规推断(类似于模板推断,相当于T)

c++11,声明变量时根据初始值自动选择匹配的类型,可与指针、引用、const合用

【方法】先推断变量的类型,再由此推auto的类型!

1、传值方式(非指针、非引用):引用属性和const属性都被抛弃,看成新副本

auto x = 27;    x=int,auto=int
const auto x2 = x;    x2=const int,auto=int

2、指针或引用类型:抛弃引用、不抛弃const

const auto &xy = x;    xy=const int&,auto=int
auto y = new auto(100);    y=int*,auto=int*
const auto *p = &x;    p=const int*,auto=int
auto p2 = &x;    p2=int*,auto=int*

3、万能引用类型

auto && wnyy1 = x;    x是左值,auto=int&,qnyy1=int&(折叠)
auto && wnyy2 = 100;    100是右值,auto=int,wnyy2=int&&

三、auto类型针对数组和函数的推断:往往推断成指针!

四、auto类型std::initializer_list的特殊推断

int x3 = {30};    
x3被推断成std::initializer_list<int>类型,=遇到{,先推断成initializer_list,再根据元素内容推出模板参数的类型

五、auto不适用场合举例

1、不能用于函数参数

void func(auto i,int j){}

2、不能是普通成员变量,只能是static const auto m_i = 4(而且一定要初始化)

六、auto适用场合举例

1、定义容器中的迭代器

2、无法确定参数类型时的写法

 

第6节  详解decltype含义、decltype主要用途

一、decltype含义和举例:用于推导类型

c++11,返回操作数的数据类型

1、特点

1)decltype的自动类型推断发生在编译时(和autp一样)

2)decltype不会真正计算表达式的值

2、decltype后的圆括号中是个变量:i的const和引用属性在j中都会保留

const int i = 5;
decltype(i) j = 10;

3、decltype后的圆括号中是表达式:代表的类型是返回结果的类型

int *p = new int(10);
int i = 2;
decltype(*p) j = i;    //j的类型为int &

若表达式可以作为等号左边的内容(比如*p=4是允许的),返回的就是一个引用

decltype((i)) j = i;    //j的类型为int &

4、decltype后的圆括号中是函数

decltype(testf()) tmpv = 14;    tmpv的类型是testf()的返回值类型,但不调用testf()
decltype(testf) tmpv2;    tmpv2的类型是int(void),是一种可调用对象
所以可以这么用,function<decltype(testf)> tmp = testf;,这样tmp()就是调用testf()函数

二、decltype主要用途(模板编程)

1、应付可变类型

c++98时需要类模板特化,将iterator改为const_iterator才能遍历常量容器。

c++11就可以decltype(T().begin()) iter这样定义迭代器,自动识别该容器是否为常量的。

2、通过变量表达式抽取变量类型

vector<int> myvec = {10,20,30};
decltype(myvec)::size_type mysize = myvec.size();

typedef decltype(sizeof(0)) size_t;
//等价于
typedef unsigned int size_t;

3、auto结合decltype构成返回类型后置语法

auto add(int i,int j)->decltype(i+j){
    return i+j;
}

4、decltype(auto)用法

1)用于函数返回类型

template<typename T>
decltype(auto) mydouble(T& v){    //若只有auto会忽略引用,返回右值,无法赋值
    v*=2;
    return v;
}
//调用
int a = 10;
mydouble(a) = 20;

2)用于变量声明中

int x = 1;
const int &y = x;
auto z = y;    z的类型既没有const,也没有&
decltype(auto) z2 = y;    z2都有,可以把auto丢失的类型捡回来    

 

第7节  可调用对象、std::function、std::bind

一、可调用对象

1、函数指针

void(*p)(int) = test;
p(50);

2、具有operator()成员函数的类对象

3、可被准以为函数指针的类对象

4、类成员函数的指针

A a;
void(A::*p)(int) = &A::putong;
(a.*p)(555);    //调用普通成员函数

二、std::function(不能装类成员函数的指针,其他可调用对象都能装入)

function<void(int)> f1 = test;    绑定普通函数
function<void(int)> f2 = B::test;    绑定类的静态成员函数
function<void(int)> f3 = a;    绑定仿函数(A的对象a)

三、std::bind绑定器

绑定器能够将对象和相关参数绑定到一起,构成一个仿函数。

auto bf1 = std::bind(myfunc1, 10, 20, 30);
bf1();
auto bf2 = std::bind(myfunc1, placeholders::_1, placeholders::_1, placeholders::_2);
//placeholde::_1表示myfunc1的第一个参数和第二个参数由bf2的第一个参数所绑定
//placeholde::_2表示myfunc1的第三个参数由bf2的第二个参数所绑定
bf2(55,66);
std::bind(myfunc1, placeholders::_1, placeholders::_1, placeholders::_2)(10, 20);    //直接调用

直接绑定数字是值传递的,但是用placeholders绑定的是可以变的;

void myfunc2(int &x, int &y) {
    x++;
    y++;
}
//main中
int a = 2;
int b = 3;
auto bf3 = std::bind(myfunc2, a, placeholders::_1);
bf3(b);
cout << "a = " << a << " b = " << b << endl;

绑成员函数的用法

class CT {
public:
    void myfunpt(int x, int y) {
        cout << "x = " << x << " y = " << y << endl;
        m_i = x;
    }
    int m_i;
};
//main中
CT ct;
function<void(int, int)> bf4 = bind(&CT::myfunpt, &ct, placeholders::_1, 20);
//如果这里ct的调用不加&,那么后面调用bf4时,ct对象中的m_i成员变量就无法修改
bf4(10,55555);

bind和function配合使用

CT ct;
function<int &()> bf5 = bind(&CT::m_i, &ct);	//省2次构造和析构
bf5() = 555;
void test(int i,const function<void(int)> &f){
    f(i);
}
void func(int x){
    cout<<x<<endl;
}
//main中
for(int i=0;i<10;++i){
    test(i,func);
}
//或者
auto bf = std::bind(func,placeholders::_1);
for(int i=0;i<10;++i){
    test(i,bf);
}

四、总结

bind思想:延迟调用,将可调用对象统一格式保存起来、需要的时候再调用

 

第8节  lambda表达式、for_each、find_if

一、用法简介 c++11的一种可调用对象

lambda表达式定义了一个匿名函数,可以捕获一定范围内的变量(捕获列表)。

auto f = [](int a)->int{return a+1;};
cout<<f(1)<<endl;

可以在函数内部定义:有返回类型、参数列表、函数体

[捕获列表](参数列表)->返回类型{函数体};

a)参数列表可以给初值,返回类型有时候可以省略

b)参数列表可以省略,甚至()也可以省略

c)捕获列表和函数列表不能省略!

二、捕获变量

int i = 9;
auto f = []{return i;};    报错,static的变量可以直接用
auto f = [&]{return i;};    捕获外部作用域中的所有变量,可以修改变量值
auto f = [=]{return i;};    捕获外部作用域中的所有变量,可以用值,但不可以赋值
auto f = [this]{return m_i;};    捕获当前类的this指针,lambda表达式有和当前成员函数相同的访问权限
【注】使用=和&默认使用了this
auto f = [i,j]{return i+j;};    不捕获其他变量
auto f = [&i,j]{i=5;};    可以给i赋值
auto f = [=,&i,&j]{};    按值捕获所有变量,按引用捕获后续变量
auto f = [&,i,j]{};    与上一个相反

总结:lambda表达式对于能访问的外部变量控制的非常细致!

三、lambda表达式延迟调用易出错细节分析

int i = 0;
auto f = [i] {
    return i;
};
i = 10;
cout<<f()<<endl;

凡是按值捕获的外部变量,在lambda表达式定义时,就已经复制了一份在lambda表达式变量中,所以输出还是0,解决办法是变=为&,或在捕获变量时加&。

四、lambda表达式中的mutable

int i = 0;
auto f = [i] ()mutable {
    i = 10;
    return i;
};

这里的()是不能省略的,这个mutable的作用是使i的值也是可以修改的。

五、lambda表达式的类型及存储

c++11中,lambda表达式的类型为“闭包类型”(函数内的函数),是带有operator()的类类型对象

可以用function和bind保存和调用lambda表达式,每个lambda表达式生成一个匿名对象

function<int(int)> fc1 = [](int tmp) {
    return tmp;
};
cout << fc1(10) << endl;
function<int(int)> fc2 = bind([](int tmp) {
    return tmp; 
    },placeholders::_1
);
cout << fc2(20) << endl;

不捕获任何变量的lambda表达式可以转换成一个函数指针

using pointType = int(*)(int);
pointType pt = [](int tmp) {
    return tmp;
};
cout << pt(100) << endl;

【语法糖】一种简便写法而已,lambda表达式也可以看成一种语法糖。

int a[5];
*(a+1)=3;用a[1]=3;替代

六、lambda表达式再演示和优点总结

1、for_each简介(函数模板) algorithm

vector<int> myvec = { 10,20,30,40,50 };
int sum = 0;
//for_each(myvec.begin(), myvec.end(), myfunc);
for_each(myvec.begin(), myvec.end(), [&sum](int val) {
    sum += val;
    cout << val << endl;
});
cout << "sum = " << sum << endl;

2、find_if简介:从第一个参数开始,如果第三个参数返回false,就一直遍历到第二个参数

vector<int> myvec = { 10,20,30,40,50 };
auto fi = find_if(myvec.begin(), myvec.end(), [](int val) {
    cout << val << endl;
    /*if (val > 15)
    return true;*/
    return false;
});
if (fi == myvec.end())
    cout << "not find!!!" << endl;
cout << "result = " << *(fi-1) << endl;

 

第9节  lambda表达式捕获模式的陷阱分析和展示

一、捕获列表中的&

绑了局部变量就不能在变量作用域外调用lambda表达式(这种错误也被叫做“引用悬空”,可以通过按值绑定来解决)

二、形参列表可以试用auto(c++14)

三、成员变量的捕获问题

捕获这个概念只针对在创建lambda表达式的作用域内可见的非静态局部变量,故成员变量m_at是无法被捕获的

vector<function<bool(int)>> gv;
class AT {
public:
    int m_at = 7;
    //解决办法
    void addItem(){
        auto matCopy = m_at;    //解决办法!!!
        gv.push_back(
            //[=](auto tmp) {	//似乎不是按值捕获的
            //这个lambda表达式是依赖于this指针的,也就是依赖于对象的,所以delete后就失效了
            [matCopy](auto tmp){
                cout << matCopy << endl;
                if (tmp % matCopy == 0)
                    return true;
                return false;
            }
        );
    }    
};
void func() {
    AT *pat = new AT();
    pat->addItem();
    delete pat;
    //pat不存在,lambda失效
    cout<<gv[0](10)<<endl;
}

四、广义lambda捕获(c++14)

直接在[ ]里,[abc = 成员变量名]

五、静态局部变量:保存在静态存储区,直接用就可以,类似于按引用捕获的效果

【提示】慎用静态局部变量!

 

第10节  可变参数函数、initializer_list、省略号形参

一、可变参数函数:能接受非固定参数个数参数

initializer_list标准库类型:使用前提——所有实参类型相同

二、initializer_list(初始化列表) c++11类模板

1、初始化后不能被改变

initializer_list<string> array1 = { "aa","bb","cc" };

2、拷贝和赋值:不会拷贝和复制array1中的元素,对象地址不同但指向的元素相同,共享同一份数据!

initializer_list<string> array2(array1);    //array2和array1指向的首地址相同

3、初始化列表做普通函数参数和构造函数参数

void printInfo(initializer_list<string> tmpList) {
    /*for (auto iter = tmpList.begin(); iter != tmpList.end();++iter){
        cout << (*iter).c_str() << endl;
    }*/
    //范围for
    for (auto &tmpitem : tmpList) {
        cout << tmpitem.c_str() << endl;
    }
    cout << tmpList.size() << endl;
}
//调用
printInfo({ "ASDFADS","SAFSDFASD" });
class CT {
public:
    explicit CT(const initializer_list<int> &tmp) {
        //explicit禁止了隐式类型转换
    }
};
//调用
//CT ct = { 10,20,30 };	//隐式类型转换
CT ct{ 10,20,30 };

三、省略号形参(...)

int和char*比较适用,需加入#include "stdarg.h"

例子1:

double average(int num, ...) {
    va_list valist;
    double sum = 0;
    va_start(valist, num);	//使valist指向起始的参数
    for (int i = 0; i < num; ++i) {
        sum += va_arg(valist, int);    //这里绑定好后va_arg这个东西应该自动从num的下一个元素开始遍历
    }
    va_end(valist);    //释放list
    return sum;
}
//调用
cout << average(3, 55, 47, 95) << endl;

例子2:

void test(const char *msg, ...) {
    va_list valist;
    int csgs = atoi(msg);
    va_start(valist, msg);	//使valist指向起始的参数,绑的是离...最近的参数
    int count = 0;
    while (count < csgs) {
        char *p;
        p = va_arg(valist, char*);
        printf("第%d个参数是:%s\n", count, p);
        count++;
    }
    va_end(valist);    //释放list
}
//调用
test("3", "qqweq", "qweqw", "erqw");

【注意事项】

a)至少有一个有效形参(普通参数)

b)...形参只能出现在形参列表最后

c)...之前的可以省略

d)若有多个普通参数,va_start(valist, msg)的第二个参数绑...之前的那个参数

e)可变参数类型一般为int和char*

f)不建议使用,但要能看懂!

 

第11节  萃取(traits)技术概念、范例等

一、类型萃取概述(type traits):泛型编程,stl源码中用的比较多

c++11中提供了很多了很多类型萃取的接口,这些接口其实就是一些类模板

二、类型萃取范例

通过萃取接口中的value值为true或者false可以萃取出对象、类、容器的很多有用信息(如,是否为对象,是否含虚函数等)。

三、迭代器萃取概述(iterators traits)

给定一个迭代器的类型对象,能够萃取出该迭代器的类型。

四、总结

c++中模板与泛型编程和模板元编程这种编程方法用的并不多,这种方法一般用于开发标准库和模板库,实际工作中一般都是开发业务逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值