C++11的更新介绍(lamada、包装器)

2024-04-13 15:28
文章标签 c++ 介绍 更新 包装 lamada

本文主要是介绍C++11的更新介绍(lamada、包装器),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

 

🪐🪐🪐欢迎来到程序员餐厅💫💫💫

          主厨:邪王真眼

主厨的主页:Chef‘s blog  

所属专栏:c++大冒险

总有光环在陨落,总有新星在闪烁


lambda表达式

C++98中的一个例子

在C++98中,如果想要进行排序,可以使用std::sort方法如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

struct Goods
{string _name;  // 名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}
};
struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};
int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 
3 }, { "菠萝", 1.5, 4 } };sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
}

随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,

都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,

这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。


lambda表达式语法

  • lambda表达式书写格式:

[capture-list] (parameters) mutable -> return-type { statement }

  1. [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来 判断接下来的代码是否为lambda函数捕捉列表能够捕捉上下文中的变量供lambda函数使用
  2. (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  3. mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  4. ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
  5. {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获 到的变量。
  • 格式省略情况:

1.mutable可省略

int a = 0, b = 0;
auto func = [a, b]()->int {return 0; };

2.返回值类型可省略,编译器自动推导

int a = 0, b = 0;
auto func = [a, b]() {return 0; };

3.没有传参时列表可省略

int a = 0, b = 0;
auto func = [a, b] {return 0; };

4.而捕捉列表和函数体可以为空。

 // 省略了返回值类型,无返回值类型auto fun1 = [](int c){ }; 

因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

lambda表达式返回值实际上是仿函数,

该仿函数无法直接调用,如果想要调用,可借助auto将其赋值给一个变量,lambda返回的仿函数对象,其类名是随机的,因此必须用auto来接受这个仿函数对象。


捕获列表说明

捕捉列表描述了父作用域中那些数据可以被lambda使用,以及使用的方式传值还是传引用

1.[var]:表示值传递方式捕捉变量var

int a = 0, b = 0;
auto func = [a, b] (){return a+b};

注意:此时a,b具有常性,如果要去掉他们的常性就要加上mutable,此时lambda获得ab的方式是传值,所以不会影响到父作用域的ab

int a = 0, b = 0;
auto func = [a, b] ()mutable{return a+b};

2.[this]:表示值传递方式捕捉当前的this指针

3.[=]:表示值传递方式捕获所有父作用域中的变量(包括this)

int a = 0, b = 0;
auto func = [=]()mutable {return a + b; };

4.[&var]:表示引用传递捕捉变量var

此时lambda获得ab的方式是传引用,在lambda里修改ab会影响父作用域的ab

int a = 0, b = 0;
auto func = [&a,&b]{return a + b; };

5.[&]:表示引用传递捕捉所有父作用域中的变量(包括this)

int a = 0, b = 0;
auto func = [&]() {return a + b; };

6.我们还可以把传值和传引用混合使用,让部分参数传参,部分参数传引用

[x, &y]:以传值的形式捕获x,以传引用的形式捕获y
[=, &x]:以传值的形式捕获父作用域所有变量,以传引用的形式捕获x
[&, x]:以传值的形式捕获x,以传引用的形式捕获父作用域所有变量

注意:

  1.  父作用域指包含lambda函数的语句块
  2. c捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
  3. 在块作用域以外的lambda函数捕捉列表必须为空
  4. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
  5.  lambda表达式之间不能相互赋值,因为他们的实际类名不同(每一个类名都是lamdba随机生成的)

有lambda后,我们在需要仿函数的地方,就无需额外写一个仿函数的类,而是直接写lambda表达式:

int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 
3 } };sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._price < g2._price; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._price > g2._price; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate < g2._evaluate; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate > g2._evaluate; });
}

函数对象与lambda表达式

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的

类对象。

class Rate
{
public:Rate(double rate): _rate(rate){}double operator()(double money, int year){ return money * _rate * year;}
private:double _rate;
};
int main()
{
// 函数对象double rate = 0.49;Rate r1(rate);r1(10000, 2);
// lamberauto r2 = [=](double monty, int year)->double{return monty*rate*year; 
};r2(10000, 2);return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。

函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可

以直接将该变量捕获到。 ​

  

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如

果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator(),而这个类的名字是编译器随机产生的,所以lambda表达式之间不能相互赋值,即使看起来类型相同,因为他们的类名是不同的


模板参数中的lambda表达式

我们要给set传一个仿函数

set<int, Less<int>>;

请注意,这里我们所传的不是对象,而是类,但是lambda返回值本身就是对象,所以直接传lambda肯定是不可以的

这个时候decltype就登场了

auto func = [](int a, int b) {return a < b; };
set<int, decltype(func)>;

包装器

function包装器 也叫作适配器。C++中的function本质是一个类模板

为什么需要function

如果一个变量f,可以按f()的形式调用函数,那么称f是一个可调用对象

基于此不难想到可调用对象包括:函数、仿函数、lambda

我们来看看他们在下面代码的表现

template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}
double f(double i)
{return i / 2;
}
struct Functor
{double operator()(double d){return d / 3;}
};
int main()
{// 函数cout << useF(f, 11.11) << endl;// 函数对象cout << useF(Functor(), 11.11) << endl;// lamber表达式cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;return 0;
}

通过上面的程序验证,我们会发现useF函数模板实例化了三份

然而,这里的函数、仿函数、lambda的返回值和参数类型相同,可以认为三者极其相似,有没有办法让函数模板把他们识别为一种类型,从而只需要实例化一份呢

包装器可以很好的解决上面的问题,function包含在头文件<functional>中,是一个类模板,模板原型如下:

template <class T> function;template <class Ret, class... Args>
class function<Ret(Args...)>;

其语法为:function<返回值(参数列表)>,只要是返回值和参数列表相同的可调用对象,经过这一层封装,都会变成相同的类型。 

int f(int a, int b)
{return a + b;
}
struct Functor
{
public:int operator() (int a, int b){return a + b;}
};
template<class F>
void Function(F f)
{static int count = 0;cout << count++<<endl;cout << &count << endl;cout << "================="<<endl;
}
int main()
{// 函数名(函数指针)function<int(int, int)> func1 = f;Function(func1);// 函数对象function<int(int, int)> func2 = Functor();Function(func1);// lamber表达式function<int(int, int)> func3 = [](const int a, const int b){return a + b; };Function(func1);
}

可以看出func1、func2、func3被认为是一种类型


function接收对象成员函数

class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}
};
int main()
{// 类的成员函数function<int(int, int)> func4 = &Plus::plusi;function<double(Plus*, double, double)> func5 = &Plus::plusd;
function<double(Plus, double, double)> func5 = &Plus::plusd;
}

注意事项:

  1. 等号右边的&符号别忘了写,对于非静态函数必需加上,非静态最好加上
  2. 等号右边的函数要指定类域
  3. 对于非静态函数,左边的第一个参数是类名或类指针

    适用场景:

比如力扣:波兰表达式

对于相邻两数我们要以case语句对+-*/进行讨论

class Solution {
public:
int evalRPN(vector<string>& tokens) {stack<int> st;for(auto& str : tokens){if(str == "+" || str == "-" || str == "*" || str == "/"){int right = st.top();st.pop();int left = st.top();st.pop();switch(str[0]){case '+':st.push(left+right);break;case '-':st.push(left-right);break;case '*':st.push(left*right);break;case '/':st.push(left/right);break;}}//..........}return st.top();
}
};

使用包装器以后的玩法 

class Solution {
public:
int evalRPN(vector<string>& tokens) {stack<int> st;map<string, function<int(int, int)>> opFuncMap ={{ "+", [](int i, int j){return i + j; } },{ "-", [](int i, int j){return i - j; } },{ "*", [](int i, int j){return i * j; } },{ "/", [](int i, int j){return i / j; } }};for(auto& str : tokens){if(opFuncMap.find(str) != opFuncMap.end()){int right = st.top();st.pop();int left = st.top();st.pop();st.push(opFuncMap[str](left, right));}//........}return st.top();
}
};

冷知识:

  • function 类型相同的对象可以相互赋值

function<int(int)> f1 = [](int x) { return x * x; };
function<int(int)> f2 = f1; // f2 现在也是一个 lambda 表达式
  • function实现了对bool的重载

opearotr bool函数重载方式如下

class A
{
public:operator bool(){return 3==_a;}int _a = 2;
};int main()
{A a;if (a)cout << "666" << endl;else{cout << "888" << endl;}
}

  

function 对象支持 bool 类型转换,可以用于判断 function 对象是否为空(未初始化)。

function<int(int)> f;
if (!f) {cout << "f is empty" << endl;
}

  


bind

bind音译即是绑定,它是C++标准库中的一个函数模板,用于将函数与其参数进行绑定,生成一个新的可调用对象。通过bind,我们可以将函数的部分参数固定下来,也可以交换参数位置,从而得到一个新的函数对象。

// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2) 
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

调用bind的一般形式

auto newCallable = bind(callable,arg_list);

newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数

  • 交换参数位置

交换了第一个参数和第二个参数的位置

C++11后新增一个命名空间域placeholders,其内部会存储很多变量,这些变量用于函数的传参

placeholders::_n表示原函数中的第n个参数

#include <functional>
void Mod(int a, int b)
{cout << a % b<<endl;
}
int main()
{std::function<void(int, int)> func1 = std::bind(Mod, placeholders::_2,placeholders::_1);func1(2, 1);Mod(2, 1);return 0;
}

  


  •   给参数绑定固定值

void Mod(int a, int b)
{cout << a % b<<endl;
}
int main()
{std::function<void(int,int)> func1 = std::bind(Mod, 1, placeholders::_2);func1(2, 10);Mod(1, 10);return 0;
}

  

可以看出即使我们在func1中给第一个参数传参为2,实际也依旧是1,

三种写法:

function<void(int,int)> func1 = std::bind(Mod, 1, placeholders::_1);
func1(1,10);
function<void(int)> func2 = std::bind(Mod, 1, placeholders::_1);
func2(10);
auto func3 = std::bind(Mod, 1, placeholders::_1);
func3(1, 10);
func3(10);

  我建议是auto,它的适用范围最大


🥰创作不易,你的支持对我最大的鼓励🥰

🪐~ 点赞收藏+关注 ~🪐

e3ff0dedf2ee4b4c89ba24e961db3cf4.gif

这篇关于C++11的更新介绍(lamada、包装器)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/900505

相关文章

MySQL新增字段后Java实体未更新的潜在问题与解决方案

《MySQL新增字段后Java实体未更新的潜在问题与解决方案》在Java+MySQL的开发中,我们通常使用ORM框架来映射数据库表与Java对象,但有时候,数据库表结构变更(如新增字段)后,开发人员可... 目录引言1. 问题背景:数据库与 Java 实体不同步1.1 常见场景1.2 示例代码2. 不同操作

C++ 中的 if-constexpr语法和作用

《C++中的if-constexpr语法和作用》if-constexpr语法是C++17引入的新语法特性,也被称为常量if表达式或静态if(staticif),:本文主要介绍C++中的if-c... 目录1 if-constexpr 语法1.1 基本语法1.2 扩展说明1.2.1 条件表达式1.2.2 fa

MySQL中慢SQL优化的不同方式介绍

《MySQL中慢SQL优化的不同方式介绍》慢SQL的优化,主要从两个方面考虑,SQL语句本身的优化,以及数据库设计的优化,下面小编就来给大家介绍一下有哪些方式可以优化慢SQL吧... 目录避免不必要的列分页优化索引优化JOIN 的优化排序优化UNION 优化慢 SQL 的优化,主要从两个方面考虑,SQL 语

C++中::SHCreateDirectoryEx函数使用方法

《C++中::SHCreateDirectoryEx函数使用方法》::SHCreateDirectoryEx用于创建多级目录,类似于mkdir-p命令,本文主要介绍了C++中::SHCreateDir... 目录1. 函数原型与依赖项2. 基本使用示例示例 1:创建单层目录示例 2:创建多级目录3. 关键注

C++从序列容器中删除元素的四种方法

《C++从序列容器中删除元素的四种方法》删除元素的方法在序列容器和关联容器之间是非常不同的,在序列容器中,vector和string是最常用的,但这里也会介绍deque和list以供全面了解,尽管在一... 目录一、简介二、移除给定位置的元素三、移除与某个值相等的元素3.1、序列容器vector、deque

C++常见容器获取头元素的方法大全

《C++常见容器获取头元素的方法大全》在C++编程中,容器是存储和管理数据集合的重要工具,不同的容器提供了不同的接口来访问和操作其中的元素,获取容器的头元素(即第一个元素)是常见的操作之一,本文将详细... 目录一、std::vector二、std::list三、std::deque四、std::forwa

C++字符串提取和分割的多种方法

《C++字符串提取和分割的多种方法》在C++编程中,字符串处理是一个常见的任务,尤其是在需要从字符串中提取特定数据时,本文将详细探讨如何使用C++标准库中的工具来提取和分割字符串,并分析不同方法的适用... 目录1. 字符串提取的基本方法1.1 使用 std::istringstream 和 >> 操作符示

C++原地删除有序数组重复项的N种方法

《C++原地删除有序数组重复项的N种方法》给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度,不要使用额外的数组空间,你必须在原地修改输入数组并在使用O(... 目录一、问题二、问题分析三、算法实现四、问题变体:最多保留两次五、分析和代码实现5.1、问题分析5.

C++ 各种map特点对比分析

《C++各种map特点对比分析》文章比较了C++中不同类型的map(如std::map,std::unordered_map,std::multimap,std::unordered_multima... 目录特点比较C++ 示例代码 ​​​​​​代码解释特点比较1. std::map底层实现:基于红黑

C++中函数模板与类模板的简单使用及区别介绍

《C++中函数模板与类模板的简单使用及区别介绍》这篇文章介绍了C++中的模板机制,包括函数模板和类模板的概念、语法和实际应用,函数模板通过类型参数实现泛型操作,而类模板允许创建可处理多种数据类型的类,... 目录一、函数模板定义语法真实示例二、类模板三、关键区别四、注意事项 ‌在C++中,模板是实现泛型编程