上篇文章(C++11的新特性(上))我们讲述了C++11中的部分重要特性。本篇接着上篇文章进行讲解。本篇文章主要进行讲解:完美转发、新类的功能、可变参数模板、lambda 表达式、包装器。希望本篇文章会对你有所帮助。
文章目录
一、完美转发
1、1 实例详解
1、2 应用场景
二、新类的功能
2、1 默认成员函数
2、2 缺省参数初始化
2、3 强制生成默认函数的关键字:default
2、4 禁止生成默认函数的关键字:delete
2、5 继承和多态中的final和override关键字
三、可变参数模板
3、2 递归函数方式展开参数包
3、2 逗号表达式展开参数包
3、3 STL容器中的empalce相关接口函数
四、lambda 表达式
4、1 C++98例子引入
4、2 lambda 表达式详解
4、2、1 lambda 表达式语法
4、2、2 lambda 表达式实例
4、2、3 lambda 表达式与函数对象(仿函数)
五、包装器
5、1 function包装器用法
5、2 function包装器举例使用
5、3 bind 捆绑器
5、3、1 bind 绑定参数
5、3、2 bind绑定 交换参数顺序
🙋♂️ 作者:@Ggggggtm 🙋♂️
👀 专栏:C++ 👀
💥 标题:C++11 💥
❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️
一、完美转发
1、1 实例详解
上衣拍案文章末尾我们学习了右值引用。那么右值引用加上模板会出现一种特殊的情况。我们先看效果,代码如下:
void Fun(int& x) { cout << "左值引用" << endl; } void Fun(const int& x) { cout << "const 左值引用" << endl; } void Fun(int&& x) { cout << "右值引用" << endl; } void Fun(const int&& x) { cout << "const 右值引用" << endl; } template<typename T> void PerfectForward(T&& t) { Fun(t); } int main() { PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b = 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }
我们直接看输出结果:
怎么全部是左值或左值引用呢? 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。但是不管是接收的左值还是右值,都会将其实参绑定到形参的左值引用上(引用折叠)。引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 这时就需要引入完美转发了。
完美转发的具体用法:std::forward 完美转发在传参的过程中保留对象原生类型属性。下面我们改写一下上述的代码进行理解:
void Fun(int& x) { cout << "左值引用" << endl; } void Fun(const int& x) { cout << "const 左值引用" << endl; } void Fun(int&& x) { cout << "右值引用" << endl; } void Fun(const int&& x) { cout << "const 右值引用" << endl; } template<typename T> void PerfectForward(T&& t) { // std::forward<T>(t)在传参的过程中保持了t的原生类型属性。 Fun(std::forward<T>(t)); } int main() { PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b = 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }
运行结果如下图:
1、2 应用场景
我们之前模拟实现过 list 的底层。当我们学完右值引用后,我们再看一下 list 的底层。代码如下:
template<class T> struct ListNode { ListNode* _next = nullptr; ListNode* _prev = nullptr; T _data; }; template<class T> class List { typedef ListNode<T> Node; public: List() { _head = new Node; _head->_next = _head; _head->_prev = _head; } void PushBack(T&& x) { //Insert(_head, x); Insert(_head, std::forward<T>(x)); } void PushFront(T&& x) { //Insert(_head->_next, x); Insert(_head->_next, std::forward<T>(x)); } void Insert(Node* pos, T&& x) { Node* prev = pos->_prev; Node* newnode = new Node; newnode->_data = std::forward<T>(x); // 关键位置 // prev newnode pos prev->_next = newnode; newnode->_prev = prev; newnode->_next = pos; pos->_prev = newnode; } void Insert(Node* pos, const T& x) { Node* prev = pos->_prev; Node* newnode = new Node; newnode->_data = x; // 关键位置 // prev newnode pos prev->_next = newnode; newnode->_prev = prev; newnode->_next = pos; pos->_prev = newnode; } private: Node* _head; }; int main() { List<string> lt; lt.PushBack("1111"); lt.PushFront("2222"); return 0; }
上述代码中,关键点在于插入添加了右指引用的接口。当然,上述情况的 list 在插入内置类型(int、char、double……)时并没有任何影响。但是当我们插入的是自定义类型呢?就上述的例子解释:
为什么前面说内置类型并没有任何影响,但是自定义类型就不同了呢?注意:在调用 Insert 函数时,如果没有完美转发的话,x退化为左值。进而会调用参数为左值引用的 Insert 函数。但是这样底层在插入数据时,会多出来一次拷贝构造。而如果使用完美转发保持参数的原有属性时,底层会进行移动构造。进而会提升效率。
二、新类的功能
2、1 默认成员函数
我们之前在初学C++类时,有6个默认成员函数
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
- 构造函数;
- 析构函数;
- 拷贝构造函数;
- 拷贝赋值重载;
- 取地址重载;
- const 取地址重载。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
2、2 缺省参数初始化
我们之前在学类和对象时,就学过了参数可以给缺省值。这个功能是C++11新增的,这里就不再过多详细解释了。
2、3 强制生成默认函数的关键字:default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。使用实例如下:
class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} Person(const Person& p) :_name(p._name) , _age(p._age) {} Person(Person && p) = default; private: bit::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); return 0; }
2、4 禁止生成默认函数的关键字:delete
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明就可以。这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。使用实例如下:
class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} Person(const Person& p) = delete; private: bit::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); return 0; }
2、5 继承和多态中的final和override关键字
在C++中,final关键字用于修饰类、成员函数,用于表示它们是最终的,不能被继承或覆盖。
- 修饰类:当在类声明时使用final关键字,表示该类是最终的,不能被其他类继承。这样一来,其他类将无法派生出继承自该类的子类。
class Base final { // class definition }; class Derived : public Base { // 错误!无法派生自标记为final的类 // class definition };
- 当在C++中使用final关键字修饰成员函数时,它的作用是表示该成员函数是最终的,不能在派生类中被覆盖或重写。
class Base { public: virtual void foo() final { // 确定的行为实现 } }; class Derived : public Base { public: void foo() { // 错误!由于被标记为final,无法在派生类中重写foo函数 // 可以直接使用基类中定义的行为实现 } };
在C++中,override关键字用于显式地标记派生类中的成员函数,表示该函数是对基类中同名函数的重写。
当我们在派生类中使用override关键字修饰一个成员函数时,编译器会检查该函数是否满足以下条件:
- 函数必须是虚函数或纯虚函数。
- 函数在基类中必须有相同的名称、返回值和参数列表。
如果派生类中的函数没有满足以上两个条件中的任意一个,编译器将产生编译错误。这样,我们可以确保在派生类中重写的函数与基类中的函数一致,避免了潜在的错误或误用。下面是一个示例:
class Base { public: virtual void foo() { // 基类的实现 } }; class Derived : public Base { public: void foo(int n) override { // 错误! 并没有完成重写 } };
三、可变参数模板
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。但是我们也需要了解一下其简单使用放法。
当涉及到处理不确定数量的参数时,C++11的可变参数模板非常有用。它提供了一种灵活的方式来定义接受任意数量参数的函数模板或类模板。以下是一些示例,解释了如何使用C++11的可变参数模板:
template <class ...Args> void ShowList(Args... args) {}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
当然,我们再调用此函数时,可以传入任何数量的参数。我们再看下述代码:
template <class ...Args> void ShowList(Args... args) { //计算传入的参数个数 cout << sizeof...(args) << endl; // 下述的打印实参的方法是错误的 //for (size_t i = 0; i < sizeof...(args); ++i) //{ // cout << args[i] << " "; //} //cout << endl; } int main() { string str("hello"); ShowList(); ShowList(1); ShowList(1, 'A'); ShowList(1, 'A', str); return 0; }
问题来了,到底怎么取出参数包中的参数呢?我们接着往下看。
3、2 递归函数方式展开参数包
我们先看如下代码:
#include <iostream> // 基本情况:没有额外参数时终止递归 void print() { std::cout << std::endl; } // 递归情况:打印参数并继续递归 template<typename T, typename... Args> void print(const T& firstArg, const Args&... args) { std::cout << firstArg << " "; print(args...); // 递归调用print函数 } int main() { print(1, 2, 3, "Hello", 4.5); // 调用print函数,打印多个参数 return 0; }
在上面的示例代码中,我们定义了一个print函数,它采用可变参数模板的形式。该函数的基本情况是没有额外参数时,打印一个换行符并终止递归。递归情况下,它会打印第一个参数,然后通过递归调用print函数来处理剩余的参数。
在main函数中,我们调用了print函数,并传递了多个参数(整数、字符串和浮点数)。这些参数被逐个打印出来,最终结果是"1 2 3 Hello 4.5"。
3、2 逗号表达式展开参数包
template <class T> void PrintArg(T t) { cout << t << " "; } //展开函数 template <class ...Args> void ShowList(Args... args) { int arr[] = { (PrintArg(args), 0)... }; cout << endl; } int main() { ShowList(1); ShowList(1, 'A'); ShowList(1, 'A', std::string("sort")); return 0; }
这种展开参数包的方式,不需要通过递归终止函数,是直接在ShowList函数体中展开的, PrintArg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
ShowList函数中的逗号表达式:(PrintArg(args), 0),也是按照这个执行顺序,先执行PrintArg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(PrintArg(args), 0)...}将会展开成((PrintArg(arg1),0), (PrintArg(arg2),0), (PrintArg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分PrintArg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
3、3 STL容器中的empalce相关接口函数
在上篇文章中(C++11的新特性(上))我们讲述到了STL中的变化。但是由于可变参数模板并没有进行详细的解释,所以把emplace相关接口放到此处进行详细讲解。
在C++11标准中,STL容器提供了emplace系列函数,用于在容器中构造对象并插入新元素。如下:
- emplace_back: emplace_back函数用于在容器的末尾直接构造一个新的元素,通过将传递给该函数的参数直接传递给元素的构造函数来完成构造。这样再某些可以避免创建临时对象和多次复制或移动操作。
- emplace: emplace函数用于在容器中指定位置(迭代器)之前插入新的元素,并通过将传递给该函数的参数直接传递给元素的构造函数来完成构造。
上述我们了解 emplace 和 emplace_back 后,我们来看看其具体用法和到底在哪些情况下有效率提升。我们先看如下代码:
class Date { public: Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) { cout << "Date(int year = 1, int month = 1, int day = 1)" << endl; } Date(const Date& d) :_year(d._year) , _month(d._month) , _day(d._day) { cout << "Date(const Date& d)" << endl; } Date& operator=(const Date& d) { cout << "Date& operator=(const Date& d))" << endl; return *this; } private: int _year; int _month; int _day; }; int main() { // 没有区别 vector<int> v1; v1.push_back(1); v1.emplace_back(2); vector<pair<string,int>> v2; v2.push_back(make_pair("sort", 1)); v2.emplace_back(make_pair("sort", 1)); v2.emplace_back("sort", 1); list<Date> lt1; lt1.push_back(Date(2022, 11, 16)); cout << "---------------------------------" << endl; lt1.emplace_back(2022, 11, 16); return 0; }
在上述代码中,我们就使用 vector来举例解释一下emplace系列函数的使用和优势。我们在 v1 中插入一些内置类型,其实并没有任何的效率提升,与push系列的函数用法、效果、效率可以说是一样的。但是在 v2 种插入 pair 对象,就会有所区别的。
当使用 push_back 插入自定义类型对象时,首先我们需要构造处对象。其次再插入的时候,底层在插入时采用的是拷贝对象进行插入。但是 emplace_back 插入对象,我们传入的是可变参数,并不用构造出pair对象,底层会自动识别出 pair 对象。底层在插入时,会直接把我们传入的参数进行构造到所要插入的位置。相对 push_back 插入减少一次拷贝构造。
我们自己创建一个Date类进行测试,代码如上。我们看运行结果:
确实是少了一次拷贝构造。基本上所有提供emplace系列函数的容器,在插入自定义类型对象时,都会有效率提升。而内置类型并没有构造、拷贝等,所以与push系列函数一样。
四、lambda 表达式
4、1 C++98例子引入
我们知道在 algorithm 头文件中,有一个排序算法。默认排序是升序,当我们需要排降序的时候,可以通过传递第三个参数仿函数对象进行控制。具体例子如下:
#include <algorithm> #include <functional> int main() { int array[] = { 4,1,8,5,3,7,0,9,2,6 }; // 默认按照小于比较,排出来结果是升序 std::sort(array, array + sizeof(array) / sizeof(array[0])); // 如果需要降序,需要改变元素的比较规则 std::sort(array, array + sizeof(array) / sizeof(array[0]), std::greater<int>()); return 0; }
上述情况并没有任何复杂的情况。那要是对复杂自定义类型进行排序呢?我们在看如下实例:
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()); }
上述代码中,发现需要自己进行写仿函数类。假如 Goods 的属性更多,排序的情况更加复杂呢?还需要我们进行写更多了仿函数。这样有什么问题呢?
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
4、2 lambda 表达式详解
4、2、1 lambda 表达式语法
lambda表达式的基本语法如下:
[capture list](parameters) mutable -> return type { function body }
- capture list:捕获列表。捕获列表是lambda表达式的一部分,用于访问外部的变量。可以使用空括号[]表示不捕获任何变量,也可以使用方括号[变量名]来显式捕获一个或多个变量。捕获列表还可以使用值捕获和引用捕获,即使用[=]和[&]。
- parameters:参数列表。参数列表定义了传递给lambda表达式的参数,类似于函数的参数列表。可以省略参数类型,编译器会自动推导。
- mutable:在lambda表达式中,默认情况下是不允许修改被捕获的变量的。如果需要修改,则需要使用mutable关键字进行声明。
- return type:返回类型是可选的,如果省略,则编译器会自动推导返回类型。一般情况下,我们都是选择省略的,交给编译器自行推导。
- function body:函数体用于定义具体的操作和逻辑。
注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
捕获列表说明:捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this) [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
注意:
- 父作用域指包含lambda函数的语句块。
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量。
- 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复。
- 在块作用域以外的lambda函数捕捉列表必须为空。
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- f. lambda表达式之间不能相互赋值,即使看起来类型相同。
4、2、2 lambda 表达式实例
我们用一个完整的 lambda 表达式来完成一个两数求和的功能。具体代码如下:
int main() { int a = 1, b = 2; //auto f = [](int a, int b)->int {return a + b; }; // 捕捉列表 指定捕捉变量。 // 当我们指定捕捉变量时,[] 捕捉列表中的变量名必须与当前作用域的变量名相同。 // 下面是拷贝捕捉。当然,我们想要对所捕捉的变量修改时,可选择引用捕捉 [&a , &b]。 //auto f = [a, b]()->int {return a + b; }; // 捕捉列表 = ,自动采用拷贝的方式将我们所用到的变量从当前的作用域中进行捕捉(查找) // auto f=[=](a)()->int{ return a + b; }; auto f = [=]()->int { return a + b; }; cout << f() << endl; return 0; }
上述例子我们给出了三种用法。大家都是需要进行理解掌握的。当然我们再用lambda表达式解决一下上述C++98例子的问题。具体代码如下:
struct Goods { string _name; // 名字 double _price; // 价格 int _evaluate; // 评价 //... Goods(const char* str, double price, int evaluate) :_name(str) , _price(price) , _evaluate(evaluate) {} }; //struct ComparePriceLess struct Compare1 { bool operator()(const Goods& gl, const Goods& gr) { return gl._price < gr._price; } }; //struct ComparePriceGreater struct Compare2 { 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());*/ //sort(v.begin(), v.end(), Compare1()); //sort(v.begin(), v.end(), Compare2()); sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._name < g2._name;}); sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._name > g2._name;}); 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;}); }
4、2、3 lambda 表达式与函数对象(仿函数)
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象。下面我们对比一下函数对象(仿函数)与lambda 表达式的区别。如下代码:
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); // lamber auto r2 = [=](double monty, int year)->double { return monty * rate * year; }; r2(10000, 2); return 0; }
从上面看出,lambda 表达示与函数对象使用起来并无任何差别。反而 lamdba 表达式使用起来更加简单。
lambda 表达式到底是怎么实现的呢?我们大概了解一下。 当定义lambda表达式时,编译器会生成一个匿名结构体,其中包含了lambda表达式中用到的所有变量。这个结构体会重载函数调用运算符 (),使得我们可以像调用函数一样调用lambda表达式。同时,编译器还会生成代码来初始化这个结构体的成员变量,以保证在调用所生成的匿名结构体之前、所生成的匿名结构体内部能够访问正确的变量。具体如下图:
五、包装器
5、1 function包装器用法
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。那么我们来看看,我们为什么需要function呢?
C++11引入了function模板类作为一个通用的函数包装器,用于存储、传递和调用任意可调用对象(函数、函数对象、lambda表达式等)。它可以看作是一个类型安全的、灵活的函数指针。
function模板类的基本用法如下所示:
#include <iostream> #include <functional> void foo() { std::cout << "Hello, World!" << std::endl; } int add(int a, int b) { return a + b; } int main() { std::function<void()> func1 = foo; std::function<int(int, int)> func2 = add; func1(); // 调用无返回值的函数 int result = func2(3, 4); // 调用有返回值的函数 std::cout << "Result: " << result << std::endl; return 0; }
上述代码中,我们使用了function模板类来包装两个不同的函数。首先,我们声明了一个无返回值的函数`foo`和一个有返回值的函数`add`。然后,在`main`函数中,我们分别创建了两个function对象:`func1`和`func2`。
在创建function对象时,需要指定其签名(即函数类型),以便正确地匹配被包装的函数。这里,`func1`的类型为`std::function<void()>`,表示接收无参数并返回`void`的函数;而`func2`的类型为`std::function<int(int, int)>`,表示接收两个`int`类型参数并返回`int`类型的函数。
然后,我们可以通过调用function对象来使用被包装的函数。对于无返回值的函数,可以直接通过函数调用操作符`()`来执行;对于有返回值的函数,则需要将参数传递给function对象,并接收返回值。最终,我们打印了有返回值函数的结果。
5、2 function包装器举例使用
从上述例子中,我们只是了解了function包装器的使用方法,当时并不知道为什么要引入function包装器和其具体使用场景。下述我们将会结合实际例子进行讲解。
我们先看如下代码:
// ret = func(x); // 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能 // 是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下! // 为什么呢?我们继续往下看 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函数模板实例化了三份。如下图:
包装器可以很好的解决上面的问题。我们看使用包装器的代码:
#include <functional> int f(int a, int b) { return a + b; } struct Functor { public: int operator() (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() { // 函数名(函数指针) std::function<int(int, int)> func1 = f; cout << func1(1, 2) << endl; // 函数对象 std::function<int(int, int)> func2 = Functor(); cout << func2(1, 2) << endl; // lamber表达式 std::function<int(int, int)> func3 = [](const int a, const int b) {return a + b; }; cout << func3(1, 2) << endl; // 类的静态成员函数 std::function<int(int, int)> func4 = &Plus::plusi; cout << func4(1, 2) << endl; //类的非静态成员函数 std::function<double(Plus, double, double)> func5 = &Plus::plusd; cout << func5(Plus(), 1.1, 2.2) << endl; return 0; }
对上述的代码进行简单解释:
- 首先,函数f被赋值给
std::function<int(int, int)> func1
,func1可以像普通函数一样进行调用。- 其次,Functor对象被赋值给
std::function<int(int, int)> func2
,func2可以通过()运算符调用该对象,并将参数传递给operator()(int a, int b)
。- 来到lambda表达式部分,
(const int a, const int b) { return a + b; }
被赋值给std::function<int(int, int)> func3
,func3可以像函数一样进行调用。- 接下来,静态成员函数
Plus::plusi
被赋值给std::function<int(int, int)> func4
,func4可以像函数一样被调用。- 最后,非静态成员函数
Plus::plusd
被赋值给std::function<double(Plus, double, double)> func5
,但由于非静态成员函数需要一个实例来调用(注意,&不能省略),所以在调用时需要创建一个Plus对象作为参数传递给func5。- 包装非静态的成员函数时需要注意,非静态成员函数的第一个参数是隐藏this指针,因此在包装时需要指明第一个形参的类型为类的类型。
当用包装器进行封装后,我们再来看会实例化出几份。代码和运行结果如下:
#include <functional> 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() { // 函数名 std::function<double(double)> func1 = f; cout << useF(func1, 11.11) << endl; // 函数对象 std::function<double(double)> func2 = Functor(); cout << useF(func2, 11.11) << endl; // lamber表达式 std::function<double(double)> func3 = [](double d)->double { return d / 4; }; cout << useF(func3, 11.11) << endl; return 0; }
发现确实只实例化了一份模板。解决了实例出多份模板造成效率低下的问题。但是又引出了一个问题。
上述代码我们在包装 f 、 Functor 和 lambda 表达式时,被调用函数是只需要传2个参数。但是在包装plusd时,就需要传递3个参数。
假设我现在想在map容器里建立包装器和字符串对应的映射关系,这时由于被调用函数所需传入的参数不同,无法很好的建立映射关系。于是这里就需要引入 bind 捆绑器了。
5、3 bind 捆绑器
5、3、1 bind 绑定参数
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
// 原型如下: template <class Fn, class... Args> /* unspecified */ bind(Fn&& fn, Args&&... args); // with return type template <class Ret, class Fn, class... Args> /* unspecified */ bind(Fn&& fn, Args&&... args);
上述代码是 C++ 中
bind
函数的原型,它是一个模板函数,可以根据不同的参数类型进行实例化。bind
函数的原型有两个版本,其中一个版本没有指定返回类型(使用了unspecified
),另一个版本可以指定返回类型。
无返回类型版本:
Fn&& fn
:接受一个右值引用(模板的右值引用会产生引用折叠,也是万能引用),表示要绑定的可调用对象。Args&&... args
:接受一个可变数量的参数,表示要绑定给可调用对象的参数。有返回类型版本:
Ret
:表示要指定的返回类型。Fn&& fn
:接受一个右值引用,表示要绑定的可调用对象。Args&&... args
:接受一个可变数量的参数,表示要绑定给可调用对象的参数。这两个版本的
bind
函数在调用时会将传入的可调用对象与参数进行绑定,并生成一个绑定后的函数对象。需要注意的是,由于
bind
函数的返回类型是未指定的,因此我们可以使用auto
关键字来接收返回值,或者使用std::function
来显式指定返回类型。可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
调用bind的一般形式:auto newCallable = bind(callable,arg_list);其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。
只有概念似乎并不能很好的理解 bind 的使用,下面将为你提供几个具体的示例来详细解释
bind
的使用方法:
- 绑定普通函数:
#include <iostream> #include <functional> void func(int a, int b) { std::cout << "Sum: " << (a + b) << std::endl; } int main() { auto sum_func = std::bind(func, 10, std::placeholders::_1); sum_func(20); // 输出 Sum: 30 return 0; }
在这个例子中,
bind
函数绑定了函数func
并将值10作为其第一个参数。然后,通过调用sum_func(20)
来传递参数20给func
,从而实现了延迟调用。最终的输出是Sum: 30
。- 绑定成员函数:
#include <iostream> #include <functional> class MyClass { public: void print(int num) { std::cout << "Number: " << num << std::endl; } }; int main() { MyClass obj; auto print_func = std::bind(&MyClass::print, obj, std::placeholders::_1); print_func(42); // 输出 Number: 42 return 0; }
在这个例子中,
bind
函数绑定了成员函数obj
作为该成员函数的调用者。然后,通过调用print_func(42)
来传递参数42给Number: 42
。- 绑定Lambda表达式:
#include <iostream> #include <functional> int main() { int num = 10; auto lambda = [](int x) { std::cout << "Result: " << (x * x) << std::endl; }; auto square_func = std::bind(lambda, std::placeholders::_1); square_func(num); // 输出 Result: 100 return 0; }
在这个例子中,
bind
函数绑定了Lambda表达式,并将参数num
延迟传递给该Lambda表达式。通过调用square_func(num)
,实现了对参数num
进行平方运算并输出结果。最终的输出是Result: 100
。
到这里我们就可以恨到的解决我们上述所遇到的问题了。我们在看如下代码:
using namespace placeholders; int Div(int a, int b) { return a / b; } int Plus(int a, int b) { return a + b; } int Mul(int a, int b, double rate) { return a * b * rate; } class Sub { public: int sub(int a, int b) { return a - b; } }; int main() { // 调整个数, 绑定死固定参数 function<int(int, int)> funcPlus = Plus; //function<int(Sub, int, int)> funcSub = &Sub::sub; function<int(int, int)> funcSub = bind(&Sub::sub, Sub(), _1, _2); function<int(int, int)> funcMul = bind(Mul, _1, _2, 1.5); map<string, function<int(int, int)>> opFuncMap = { { "+", Plus}, { "-", bind(&Sub::sub, Sub(), _1, _2)}, { "*", bind(Mul, _1, _2, 1.5)} }; cout << funcPlus(1, 2) << endl; cout << funcSub(1, 2) << endl; cout << funcMul(2, 2) << endl; cout << opFuncMap["+"](1, 2) << endl; cout << opFuncMap["-"](1, 2) << endl; return 0; }
5、3、2 bind绑定 交换参数顺序
在C++11中,可以使用
std::bind
函数来交换函数参数的顺序。std::bind
是一个通用的函数适配器,它接受一个可调用对象,并生成一个新的可调用对象,该对象可以延迟调用原始函数,并改变传入参数的顺序。下面是一个示例,演示如何使用
std::bind
交换函数参数顺序:#include <iostream> #include <functional> void printNumbers(int a, int b) { std::cout << "a: " << a << ", b: " << b << std::endl; } int main() { // 使用std::bind交换参数顺序 auto swappedPrint = std::bind(printNumbers, std::placeholders::_2, std::placeholders::_1); // 调用交换参数顺序后的函数 swappedPrint(3, 5); // 输出:a: 5, b: 3 return 0; }
在上面的示例中,我们定义了一个名为
printNumbers
的函数,该函数接受两个整数参数,并打印它们的值。然后,我们使用std::bind
来交换参数的顺序,创建了一个新的可调用对象swappedPrint
。std::bind
的第一个参数是要适配的函数(或函数指针),然后是要传递给函数的参数。在这里,我们使用了两个特殊的占位符std::placeholders::_1
和std::placeholders::_2
来表示原始函数的第一个和第二个参数。最后,我们通过调用
swappedPrint
来调用被适配的函数,传递了两个整数参数3和5。由于std::bind
改变了参数的顺序,所以实际上会按照交换后的顺序将参数传递给原始函数,即先传递参数5,再传递参数3。因此,输出结果为:"a: 5, b: 3"。