【C++学习】C++11新特性(第三节)——可变参数模板, lambda表达式与function包装器

本文主要是介绍【C++学习】C++11新特性(第三节)——可变参数模板, lambda表达式与function包装器,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 文章前言
  • 一.可变参数模板
      • 1.什么是可变参数模板
      • 2.获取可变参数模板里参数包的方法
      • 3.可变参数模板在容器中的引用
  • 二. lambda表达式
      • 1. lambda表达式的由来
      • 2. lambda表达式
        • 1.lambda表达式语法
        • 2. 捕获列表说明
      • 3.函数对象与lambda表达式
  • 三.包装器
      • 1.***function包装器***
        • 2.普通函数,静态成员函数与非静态成员函数的包装
      • 2.bind函数

文章前言

本篇文章是C++11新特性的最后一节,主要会讲解到模板的可变参数lambda表达式function包装器的相关知识及其使用场景。

一.可变参数模板

C++11的新特性可变参数模板能够创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,而可变参数模板可以含有不固定数量的模板参数。在有些场景下较方便。

1.什么是可变参数模板

下面就是一个基本可变参数的函数模板

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>      //名字Args是可以改变的,自己取
void ShowList(Args... args)
{}

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了 0到N(N>=0)个模版参数。

template <class ...Args>     
void ShowList(Args... args)
{}
int main()
{ShowList();   //可以没有参数ShowList(1);   //可以是1个参数ShowList(1.1,std::string("hello"));     //可以是2个参数....                   //可以是N个参数//编译器实例化后void ShowList(){}void ShowList(int ){}void ShowList(double ,string ){}}

在这里插入图片描述

我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,下面我们讲解一些方法来获取参数包的值。

那为什么不能args[i]这样方式获取可变参数呢?
答:C语言的可变参数是运行时解析,可以在运行时用运行逻辑解析,但是这里是模板,是在编译时解析。

2.获取可变参数模板里参数包的方法

  1. 利用sizeof()可以计算出参数包里面的参数个数
template <class ...Args>      
void ShowList(Args... args)
{cout << sizeof...(args) << endl;   //可以计算出参数包里面的参数个数
}
int main()
{ShowList();   ShowList(1);   ShowList(1.1, std::string("hello"));     //运行结果:// 0// 1// 2
}
  1. 可以用递归函数方式展开参数包(编译时递归解析)
//终止函数
template<class T>
void _ShowList(T val)
{cout << val << endl;
}
template <class T,class ...Args>
void _ShowList(const T& val,Args... args)
{cout << val << " ";_ShowList(args...);
}
template <class ...Args>
void ShowList(Args... args)
{_ShowList(args...);
}
int main()
{ShowList(1,1,2,3,4,5);//运行结果://1 1 2 3 4 5return 0;
}

解析:
在这里插入图片描述
在这里插入图片描述

3.可变参数模板在容器中的引用

例如:在list中的尾插函数,emplace系列就是利用了可变参数模板。
如下图:
在这里插入图片描述
他们之间的区别:
push_back()与emplace_back都是尾插,不同的是,push_back()只能接受一个参数,而emplate可以接受多个参数(这里不是插入多个值)。
他们在这种场景下没有区别:

我们自己实现简单的类类似测试:
简易版string类代码:

namespace Test
{class string{public:string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "构造" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){cout << "拷贝构造" << endl;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}// 移动构造string(string&& s){cout << "移动拷贝" << endl;swap(s);}// 拷贝赋值// s2 = tmpstring& operator=(const string& s){cout << "赋值拷贝" << endl;string tmp(s);swap(tmp);return *this;}// 移动赋值string& operator=(string&& s){cout << "移动赋值" << endl;swap(s);return *this;}~string(){delete[] _str;_str = nullptr;}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0; };
}

插入的类型的参数的是单个值时:

int main()
{std::list<Test::string> lt1;Test::string s1("xxxx");lt1.push_back(s1);lt1.push_back(move(s1));cout << "---------------------------------------------" << endl;Test::string s2("xxxx");lt1.emplace_back(s2);lt1.emplace_back(move(s2));cout << "---------------------------------------------" << endl;lt1.push_back("xxxx");lt1.emplace_back("xxxx");cout <<"---------------------------------------------"<< endl;return 0;
}
//运行结果:
//构造
//拷贝构造
//移动拷贝
//--------------------------------------------
//构造
//拷贝构造
//移动拷贝
//--------------------------------------------
//构造
//移动拷贝
//构造
//--------------------------------------------

总结
根据上面的测试可以发现:(对于插入类型的参数是单个的来说)

  • 当插入的是有名对象和匿名对象来讲,他们之间没有区别;
  • 当直接插入对象的参数时,区别不是很大,对于push_back只是多了一个构造;

当插入的类型的参数是多个值时,比如piar类型
如:

int main()
{std::list<pair<Test::string, Test::string>> lt2;pair<Test::string, Test::string> kv1("xxxx", "yyyy");lt2.push_back(kv1);lt2.push_back(move(kv1));cout <<"---------------------------------------------" << endl;pair<Test::string, Test::string> kv2("xxxx", "yyyy");lt2.emplace_back(kv2);lt2.emplace_back(move(kv2));cout << "---------------------------------------------" << endl;lt2.emplace_back("xxxx", "yyyy");cout << "---------------------------------------------" << endl;return 0;
}
//运行结果:
//构造
//构造
//拷贝构造
//拷贝构造
//移动拷贝
//移动拷贝
//---------------------------------------------
//构造
//构造
//拷贝构造
//拷贝构造
//移动拷贝
//移动拷贝
//---------------------------------------------
//构造
//构造
//---------------------------------------------

总结:
根据上面的测试可以发现:(对于插入类型的参数是多个的来说)

  • 当插入的是有名对象和匿名对象来讲,他们之间没有区别;
  • push_back不能直接传入类型的参数(因为类插入对象的类型的参数为多个);
  • 但是emplace_back可以直接传插入对象的参数(因为emplace_back利用的是可变参数模板),并且直接构造。在这方面上,emplace_back有优势;

emplace系列在插入操作(如上面研究的),直接传参数的时候效率更高,可以直接构造,当传有名对象与匿名对象时,差别不大;

二. lambda表达式

1. lambda表达式的由来

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

例如:

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;}
};
struct Goods
{string _name;    // 名字double _price;   // 价格int _evaluate;   // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
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表达式。

2. lambda表达式

1.lambda表达式语法

lambda表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement }
[捕捉列表] (参数列表) mutable -> 返回值类型 { 函数体}

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

假设我们要写一个ADD(实现两个int相加)的 lamdba 就应该这样写

[](int x, int y)->int {return x + y; };
[](int x, int y)->{return x + y; };  //返回值可以省略,编译器自动推

lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
例如想要调用上面的ADD函数:

int main()
{auto func = [](int x, int y)->int {return x + y; };cout << func(3, 4);return 0;
}
//运行结果:
//  7

有了lambda表达式,上面的比较问题就可以这样解决:

int main()
{//[capture-list] (parameters) mutable -> return-type { statement }vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };//按照水果的价格排序sort(v.begin(), v.end(), [](const Goods& G1, const Goods& G2)->bool {return G1._price > G2._price; });//按照水果的销量排序sort(v.begin(), v.end(), [](const Goods& G1, const Goods& G2)->bool {return G1._evaluate > G2._evaluate; });
}

上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个局部的匿名函数对象。

2. 捕获列表说明

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

  • [var]:表示值传递方式捕捉变量var。
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)。
  • [&var]:表示引用传递捕捉变量var。
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)。
  • [this]:表示值传递方式捕捉当前的this指针。

代码演示:

  1. 传值捕捉,传值+mutable,传引用捕捉
int main()
{int x = 10,y = 20;//传值捕捉   捕捉到的是的变量的拷贝 并且不能被修改 auto ADD = [x, y]{ //x++;                //不能被修改,会报错   return x + y; };  //如果想要修改,就必须加mutable  auto ADD = [x, y]()mutable{x+=10;   //函数体里面的x是被捕捉x的拷贝,这里对x的改变不影响外面的xreturn x + y; };   //返回的是30//如果想要变量本身被修改,就要传引用捕捉auto ADD = [&x, &y]{x += 10;           //x变为20return x + y; };   //返回的是40return 0;
}
  1. 传值捕捉当前域所有对象+传引用捕捉当前域所有对象+混着使用
int main()
{//传值捕捉当前域的所有对象,也不能被修改int x = 10, y = 20, m = 1, n = 2;auto ADD = [=] {//x++;   会报错,不能修改return x + y + m + n; };    //传引用捕捉当前域的所有对象auto ADD = [&] {x += 10;return x + y + m + n; };   //可以混着使用//传值捕捉所有,传引用捕捉xauto ADD = [ = , &x ] {x++;   return x + y + m + n; };    return 0;
}

注意:

  • 父作用域指包含lambda函数的语句块
  • 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
    比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 。
    [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他所有变量。
  • 捕捉列表不允许变量重复传递,否则就会导致编译错误。
    比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
  • 在块作用域以外的lambda函数捕捉列表必须为空。
  • 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都 会导致编译报错。
  • lambda表达式之间不能相互赋值,即使看起来类型相同
  • lambda对象紧禁了默认构造,但是可以拷贝构造

3.函数对象与lambda表达式

  1. lambda表达式的底层是怎么是实现的呢?

我们通过汇编简单的看一看:
测试代码:

int main()
{auto func1 = [] {cout << "hello world"; };func1();return 0;
}

在这里插入图片描述
我们根据上面的汇编可以看出,lambda表达式底层是通过调用operator()实现的,和仿函数一样。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator(),operator()的参数可以理解为就是捕捉列表的参数,捕捉就像是它的成员变量,访问捕捉就像是访问自己的成员变量。

扩展:思考:这两个lambda表达式类型相同吗?

auto f1 = [] {cout << "hello world"<<endl; };
f1();
auto f2 = [] {cout << "hello world"<<endl; };
f2();

我们通过看汇编(如下图):
在这里插入图片描述

从上图可以清楚的看到他们虽然实现的内容这些完全一样,但是他们底层是两个类型,这里也说明了为什么 lambda表达式之间不能相互赋值。只能通过auto去推演。

  • decltype的一个使用场景:
    假设要创建一个优先级队列,用自己的比较方式去实现大小堆的控制,这时候就需要在创建优先级队列时自己传一个函数的类型(仿函数),但是如果我们使用lambda表达式的话,就不知道它的类型,这时候只能使用decltype来推演类型了。
    举个例子:
auto func1 = [](const Date* p1, const Date* p2){//假设实现了日期类的大于比较};
int main()
{auto func1 = [](const Date* p1, const Date* p2) {//假设实现了日期类的大于比较};//priority_queue<Date* ,vector<Date*>, decltype(func1)> p1;  编不过//编译不过的原因为:lambda对象紧禁了默认构造,将它类型传给p1,会构造priority_queue<Date*, vector<Date*>, decltype(func1)> p1(func1);  //传func1给p1,这样就不会构造了,支持拷贝构造return 0;
}

三.包装器

1.function包装器

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
那么我们来看看,我们为什么需要function呢?

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;
}
//运行结果:
//count:1
//count:00007FF60E9505E0
//5.555
//count:1
//count:00007FF60E9505F0
//3.70333
//count:1
//count:00007FF60E9505F4
//2.7775

根据上面的代码运行结果进行分析,如果编译器只实例化了一份函数的话,静态变量count应该只有一份,并且地址一样,但是根据运行结果来看,编译器会将useF函数模板实例化了三份。
那怎么才能让编译器不实例化三分,而只是实例化一份呢?

有了包装器就可以很好的解决上面的问题 :

//std::function在头文件<functional> // 类模板原型如下 
template<class T> function;     
undefined template <class Ret, class...Args> 
class function<Ret(Args...)>; 
//模板参数说明: 
//Ret: 被调用函数的返回类型
//Args…:被调用函数的形参

解决方法:

int main()
{// 函数指针function<double(double)> func1 = f;cout << useF(func1, 11.11) << endl;// 函数对象function<double(double)> func2 = Functor();cout << useF(func2, 11.11) << endl;// lamber表达式function<double(double)> func3 = [](double d)->double { return d / 4; };cout << useF(func3, 11.11) << endl;return 0;
}
//运行结果:
//count:1
//count:00007FF6404505E0
//5.555
//count:2
//count:00007FF6404505E0
//3.70333
//count:3
//count:00007FF6404505E0
//2.7775

图解:
在这里插入图片描述

2.普通函数,静态成员函数与非静态成员函数的包装
  1. 对于普通的函数包装,直接包装即可;
  2. 对于静态成员函数,只需要在普通函数的包装中,加一个指定类域;
  3. 对于非静态成员函数,必须在函数前面加&(静态成员函数可加可不加),并且在包装的参数里面多传一个类类型,因为成员函数的参数列表中隐藏了一个this指针,成员函数也需要用类对象的指针或则对象去调用。

代码演示:

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);
}
int f(int a, int b)
{return a + b;
}
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)> func1 = f;  //函数cout << func1(1, 1) << endl;//静态成员函数function<int(int, int)> func2 = Plus::plusi; //&可加可不加cout << func2(2, 2) << endl;//非静态成员函数//非静态成员函数需要对象的指针或则对象进行调用,所以非静态成员函数需要多一个参数//第一种方式Plus plus;function<int(Plus*, double, double)> func3 = &Plus::plusd; //&必须加cout << func3(&plus, 1.1 ,11.11) << endl;//第二种方式function<int(Plus, double, double)> func4 = &Plus::plusd;  //&必须加cout << func4(Plus(),11.11, 11.11) << endl;
}

2.bind函数

std::bind函数 定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象(返回值)来“适应”原对象的参数列表。

// 原型如下:
template <class Fn, class... Args>bind (Fn&& fn, Args&&... args);template <class Ret, class Fn, class... Args> // with return type (2) bind (Fn&& fn, Args&&... args);   //fn是可调用对象    //Ret返回值(是一个可调用对象)
  • 作用一:调整可调用对象的参数的顺序(价值不大)
    代码演示:

int Sub(int x, int y)
{return x - y;
}
int main()
{cout << Sub(2, 1) << endl;   //1auto func1 = bind(Sub, placeholders::_2, placeholders::_1);cout << func1(2, 1)<<endl;   //-1return 0;
}
  • 作用二:调整可调用对象的参数的个数
#define N 100
int Mul(int n,int x, int y)
{return n*(x + y);
}
int main()
{cout << Mul(N, 2, 1);cout << Mul(N, 4, 5);//假设要计算N*(x+y)的值 因为N是确定不变的值,每次传参都要传,麻烦,就可以用bindauto func1 = bind(Mul, N,placeholders::_1, placeholders::_2);cout << func1(2, 1) << endl;   //只需要传两个参数就行了return 0;
}

这篇关于【C++学习】C++11新特性(第三节)——可变参数模板, lambda表达式与function包装器的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

MySQL中时区参数time_zone解读

《MySQL中时区参数time_zone解读》MySQL时区参数time_zone用于控制系统函数和字段的DEFAULTCURRENT_TIMESTAMP属性,修改时区可能会影响timestamp类型... 目录前言1.时区参数影响2.如何设置3.字段类型选择总结前言mysql 时区参数 time_zon

Python如何使用seleniumwire接管Chrome查看控制台中参数

《Python如何使用seleniumwire接管Chrome查看控制台中参数》文章介绍了如何使用Python的seleniumwire库来接管Chrome浏览器,并通过控制台查看接口参数,本文给大家... 1、cmd打开控制台,启动谷歌并制定端口号,找不到文件的加环境变量chrome.exe --rem

使用C#代码计算数学表达式实例

《使用C#代码计算数学表达式实例》这段文字主要讲述了如何使用C#语言来计算数学表达式,该程序通过使用Dictionary保存变量,定义了运算符优先级,并实现了EvaluateExpression方法来... 目录C#代码计算数学表达式该方法很长,因此我将分段描述下面的代码片段显示了下一步以下代码显示该方法如

C++中实现调试日志输出

《C++中实现调试日志输出》在C++编程中,调试日志对于定位问题和优化代码至关重要,本文将介绍几种常用的调试日志输出方法,并教你如何在日志中添加时间戳,希望对大家有所帮助... 目录1. 使用 #ifdef _DEBUG 宏2. 加入时间戳:精确到毫秒3.Windows 和 MFC 中的调试日志方法MFC

五大特性引领创新! 深度操作系统 deepin 25 Preview预览版发布

《五大特性引领创新!深度操作系统deepin25Preview预览版发布》今日,深度操作系统正式推出deepin25Preview版本,该版本集成了五大核心特性:磐石系统、全新DDE、Tr... 深度操作系统今日发布了 deepin 25 Preview,新版本囊括五大特性:磐石系统、全新 DDE、Tree

Python中lambda排序的六种方法

《Python中lambda排序的六种方法》本文主要介绍了Python中使用lambda函数进行排序的六种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们... 目录1.对单个变量进行排序2. 对多个变量进行排序3. 降序排列4. 单独降序1.对单个变量进行排序

基于Java实现模板填充Word

《基于Java实现模板填充Word》这篇文章主要为大家详细介绍了如何用Java实现按产品经理提供的Word模板填充数据,并以word或pdf形式导出,有需要的小伙伴可以参考一下... Java实现按模板填充wor编程d本文讲解的需求是:我们需要把数据库中的某些数据按照 产品经理提供的 word模板,把数据

Linux中Curl参数详解实践应用

《Linux中Curl参数详解实践应用》在现代网络开发和运维工作中,curl命令是一个不可或缺的工具,它是一个利用URL语法在命令行下工作的文件传输工具,支持多种协议,如HTTP、HTTPS、FTP等... 目录引言一、基础请求参数1. -X 或 --request2. -d 或 --data3. -H 或

SpringBoot基于MyBatis-Plus实现Lambda Query查询的示例代码

《SpringBoot基于MyBatis-Plus实现LambdaQuery查询的示例代码》MyBatis-Plus是MyBatis的增强工具,简化了数据库操作,并提高了开发效率,它提供了多种查询方... 目录引言基础环境配置依赖配置(Maven)application.yml 配置表结构设计demo_st

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规