【C++】C++11的魔法:不止右值引用,C++11赋予的编程神力

目录

引言

1、可变模版参数

可变模版参数的使用

包扩展

emplace系列接口

2、lambda

捕捉列表

3、包装器

function

bind

结语


引言

在上篇文章中我们学习了C++11中的部分内容,这节课我们来学习一下C++11中其他的一些很神奇的语法,当我在学完这些内容时,不得不感叹大佬的思维就是与我们这种凡人不一样。接下来我们就来了解一下吧。

1、可变模版参数

可变模版参数,顾名思义就是可以改变模版的参数,但是并不是改变模版参数的类型,这个模版本身就已经可以做到了,这里的可变指的是参数的个数可以随意改变。

早在C语言时期,我们就已经接触过了可变参数,很多人可能没有印象,最常见的例子就是printf这个函数,在我们使用其进行打印时,往往可以根据自己的需要选择性的打印多个内容,这种可以改变传参个数的参数类型我们就称为可变参数。

可变模版参数的使用

C++11在之前的基础上又支持了在模版的参数列表中定义可变参数,这就叫做可变模版参数,这个可变数量的参数我们称之为参数包,存在两种参数包:模版参数包,表示零或多个模版参数;函数参数包,表示零或多个函数参数。具体写法如下

cpp 复制代码
template<class ...Args>
void func(Args&&... args)
{
    sizeof...(args);    //计算一个参数包里有几个参数
    ....
}

在模版参数列表种的某个参数前面加上三个点 " ... " 就表示这个参数为可变参数,三个点后根的参数包名称可以不为Args,可以是任意名称,只是使用Args是一个惯例。此外sizeof在这里有了新的用法,"sizeof..." 这种写法下的sizeof不再计算字节大小,而是计算参数包内有几个参数。

总的来说,一个函数模版可以实例化出多个不同参数类型的函数,一个可变函数参数模版就可以实例化出多个不同参数个数的函数模版,有点类似于模版的模版。

包扩展

包扩展,实际上不是真的将参数包扩大,而是解析出参数包的内容,将参数包展开。因为参数包本身不支持像数组一样通过下标访问,只能用包扩展的形式访问,通过设计另一个可变参数模版,模版的第一个参数为T,第二个参数为可变参数,这样一来,将原本的参数包传给该模版函数时,参数包内的第一个参数就会析出成T,剩下的参数继续被打包成参数包,通过递归的方式将所有参数依次析出,这个过程就叫包扩展。

cpp 复制代码
void showlist()
{
    //编译器递归时的终止条件,当参数包为0个时,匹配这个函数
    cout << endl;
}

template<class T, class ...Args>
void showlist(T x, Args&&... args)
{
    cout << x << " ";
    //args时N个参数的参数包
    //调用showlist,参数包的第一个传给x,剩下N-1传给第二个参数包
    showlist(args...);
}

template <class ...Args>
void print(Args&&... args)
{
    showlist(args...);
}

注意,这里判断递归终止时不能像我们之前一样,用if语句判断,如:

cpp 复制代码
if(sizeof...(args) == 0)
{
    return;
}

因为if语句的判断方式是运行时判断,但是包扩展的过程是在编译时进行的,相当于在编译时编译器就生成了多个对应的showlist函数,并且在编译时编译器是不会去运行if语句的,如果这么写相当于编译器一直找不到参数包为0时需要实例化的函数,也就一直找不到包扩展停止的条件,就会报错,所以只能显示的写一个参数为零的重载函数来结束包扩展这个过程。

emplace系列接口

在我们对stl的学习中,有一类接口没有学过,就是emplace系列的接口,因为emplace不仅要使用右值引用(万能引用),还涉及到可变模版参数的问题,所以在没了解过这一块之前无法进行讲解,现在我们已经学完了对应的知识,可以来学一学这个接口了,看看这一系列的接口比起push和insert到底有什么优势。
push_back emplace_back

首先是传值方面,在传值方面emplace和push并没有区别,因为emplace_back的参数类型为万能引用类型,push_back也有适配右值引用的重载,所以无论是传左值还是右值,它们的都是采用一样的构造方式。

区别在于,它们在直接使用一串常量时的构造方式,push_back需要先走一层隐式转换,再进行拷贝构造,而emplace_back却不需要,这是为什么呢?下文我就用push和emplace代指两个接口了。

因为push只是一个普通的成员函数,它的数据类型在对象实例化时就已经决定了,而一串常量,这一串常量本身并不是该类型的对象,所以编译器需要先走一层隐式类型转换,将它们构造成临时对象,再用这个临时对象移动构造到原链表。但是emplace是一个可变参数模版函数,在接收常量时编译器就自动推导它们的类型,并且将它们放入参数包内,就可以直接进行构造,不用先构造临时对象。

总结:emplace系列兼容push和insert的功能,并且在特殊场景下可以省去一次隐式类型转换并直接构造,所以emplace综合下来更好用更强大,推荐使用emplace系列接口。

2、lambda

lambda表达式本质时一个匿名函数对象,跟普通函数不同的时他可以定义在函数内部。在语法使用层面,lambda并没有类型,它的类型由编译器自动生成以保证每个lambda的类型都不相等,都互相独立,所以一般情况下直接使用auto来接收lambda对象。

cpp 复制代码
//lambda表达式的格式
[capture-list] (parameters)-> return {function body}

lambda表达式的格式如上所示,其中capture-list表示捕捉列表,parameters为参数列表,->return为返回值类型,function body就是函数体。

捕捉列表

lambda表达式中默认只能使用参数列表以及函数体内定义的变量,如果想要使用外层作用域中的变量就需要进行捕捉。

第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量之间用逗号分隔。使用传值捕捉时,被捕捉的变量被认为是被const修饰的,无法修改值,引用捕捉可以修改。如果是定义在全局域中的变量就不用进行捕捉,可以直接使用。

第二种捕捉方式叫隐式捕捉,直接在捕捉列表中写一个赋值符号=就表示隐式传值捕捉,写一个引用符号就表示隐式传引用捕捉,这样才lambda表达式中使用了哪些变量,编译器就会自动去捕捉那些变量,还可以实现混合捕捉,可以将想要传值捕捉的变量显示的写出来,此外单独写个引用符号,就可以实现指定特定变量的捕捉方式,反过来也是一样。

cpp 复制代码
//传值捕捉
[a, b] (int c, int d)->int
{
    ...
}

//传引用捕捉
[&a, &b] (int c, int d)->int
{
    ...
}

//隐式捕捉
[&] (int c, int d)->int
{
    ...
}

//隐式捕捉
[=] (int c, int d)->int
{
    ...
}

//混合捕捉
[&, a] (int c, int d)->int
{
    ...
}

lambda的原理和范围for很像,编译后从汇编指令层的角度看,压根就没用lambda和范围for这样的东西,范围for的底层是迭代器,lambda的底层是仿函数对象, 也就是说我们写了一个lambda后,编译器就会自动生成一个仿函数。

仿函数的类名是编译器按一定的规则生成的,保证不同的lambda生成的类名不同,lambda参数/返回值/函数体就是仿函数operator()的参数/返回值/函数体,lambda的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,隐式捕捉编译器就会先检查一遍要用到哪些变量就传哪些。

3、包装器

function

function是一个类模板,它实例化的对象可以包装存储其他的一切可调用对象,包括函数指针、仿函数、lambda、bind表达式等,存储的可调用对象统称为std::function的目标,若不含目标,则称它为空。

函数指针、仿函数、lambda等可调用对象的类型各不相同,function的优势就是统一类型,对它们都可以进行包装,这样在很多地方就方便声明可调用对象的类型。

cpp 复制代码
//function公式
std::function<R(Args...)>  f;

R代表返回值类型,Args...代表参数列表。

说了这么多,相信很多人都会疑惑,上面讲的这什么lambda和function的听起来感觉也没什么大用啊?在这里我就用一道算法题来证明它们的作用LCR 036. 逆波兰表达式求值 - 力扣(LeetCode)

这道题简单来说就是求一个后缀表达式的值,在以往我们解决这种问题时,是不是经常需要列举多个if语句来判断算数符是什么?如

cpp 复制代码
if ( c == '+')
{ ... }
else if (c == '-')
{ ... }
...

但是我们刚才学了lambda表达式和function包装器,我们就可以将这种冗余的代码优化一下,利用哈希表的特性:

cpp 复制代码
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        map<string, function<int(int, int)>> mp = {
            {"+", [](int a, int b){return a + b;}},
            {"-", [](int a, int b){return a - b;}},
            {"*", [](int a, int b){return a * b;}},
            {"/", [](int a, int b){return a / b;}}
        };

        for (auto& str : tokens)
        {
            if (mp.count(str))
            {
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();
                int ret = mp[str](left, right);
                st.push(ret);
            }
            else
            {
                st.push(stoi(str));
            }
        }
        return st.top();
    }
};

先设置一个哈希表,key为string类型,用于存储运算符作为标志位,value值就为一个function包装器,因为之前说过我们本身是拿不到lambda表达式的类型的,所以只要我们给它套上一层封装,将其包装为function类型,就可以用function类型来表示它。

这样在接下来我们每次获取tokens的字符时,就可以直接用哈希表检查其是否是运算符,如果是的话就可以直接调用哈希表里对应的lambda表达式,大大节省了代码的篇幅和复杂度,对学过lambda表达式和function的人来说整个代码也就变的通俗易懂了。

bind

bind表达式是一个函数模版,它也是一个可调用对象的包装器,可以把它看作一个函数适配器,对可接收的可调用对象进行处理后返回一个可调用对象。

std::bind 用于把一个已有的可调用对象(函数、成员函数、仿函数等)包装成一个新的可调用对象,其核心作用是参数绑定与重排,它的基本形式是 std::bind(可调用对象, 参数...),其中参数可以写成具体值表示固定传入,也可以写成 std::placeholders::_1、_2 等占位符表示调用新对象时由外部传入的第 N 个实参,于是新对象被调用时会把这些实参按占位符的位置填回原可调用对象,从而得到原函数的调用结果,例如 auto f = std::bind(add, 10, std::placeholders::_1); 等价于 f(x) = add(10, x),把双参数的 add 变成了单参数函数,而 std::bind(add, _2, _1) 则能交换参数顺序,得到 g(x, y) = add(y, x),因此 bind 最常见的用法就是固定部分参数、调整参数顺序、适配成员函数,以及把签名不匹配的函数改造成符合 std::function 要求的形状,从而统一存储和调用,需要注意的是现代 C++ 中这类需求大多可以用 lambda 更清晰地表达,bind 更适合参数重排或快速适配已有函数的场景。

结语

这节课我们完成了对C++11剩余知识的学习,还剩下一个智能指针我们下一篇文章再进行讲解。不得不说C++11不愧是C++最重大的一次更新,很多新内容都非常的有用,设计也十分巧妙,学会熟练使用C++11的内容可以让我们的代码水平更上一层楼。

相关推荐
码匠许师傅1 小时前
【C++三方组件】RE2:工业级正则的安全与性能
c++
ThornArmor1 小时前
《向内深潜,向外飞掠》
c语言·开发语言·c++·vim·visual studio
j7~2 小时前
【C++微服务项目开发脚手架】(环境篇)虚拟机 + Docker + MySQL/Redis/RabbitMQ/ES/etcd/FastDFS 全套配齐
linux·c++·ubuntu·docker·vmware·项目开发·微服务脚手架
qq_199886872 小时前
第5板块·第4节:性能优化中的高级技巧
c++·人工智能·gpu算力
郝学胜_神的一滴2 小时前
C++20模板元编程 04:变量模板 别名模板与模板Lambda实战
c++·visual studio
Methy2 小时前
一次线上死锁排查:std::list::size () 居然是 O (n)?
c++·后端
2401_868534782 小时前
利弊比较类 社会生活类
c++·需求分析
wuminyu2 小时前
纯轻量级锁体系下C2编译器锁粗化和消除机制剖析
java·linux·c语言·jvm·c++
weilx12342 小时前
C++笔记-mutex
c++