当前位置: 首页 > news >正文

【C++】C++11新特性(下)

 

  上篇文章(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 posprev->_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 posprev->_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个默认成员函数

  1. 构造函数;
  2. 析构函数;
  3. 拷贝构造函数;
  4. 拷贝赋值重载;
  5. 取地址重载;
  6. const 取地址重载。
  最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。

  C++11 新增了两个:移动构造函数和移动赋值运算符重载针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

  1. 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  2. 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

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关键字用于修饰类、成员函数,用于表示它们是最终的,不能被继承或覆盖。

  1. 修饰类:当在类声明时使用final关键字,表示该类是最终的,不能被其他类继承。这样一来,其他类将无法派生出继承自该类的子类。
    class Base final {// class definition
    };class Derived : public Base { // 错误!无法派生自标记为final的类// class definition
    };
  2. 当在C++中使用final关键字修饰成员函数时,它的作用是表示该成员函数是最终的,不能在派生类中被覆盖或重写。
    class Base {
    public:virtual void foo() final {// 确定的行为实现}
    };class Derived : public Base {
    public:void foo() {// 错误!由于被标记为final,无法在派生类中重写foo函数// 可以直接使用基类中定义的行为实现}
    };

  在C++中,override关键字用于显式地标记派生类中的成员函数,表示该函数是对基类中同名函数的重写。

  当我们在派生类中使用override关键字修饰一个成员函数时,编译器会检查该函数是否满足以下条件:

  1. 函数必须是虚函数或纯虚函数。
  2. 函数在基类中必须有相同的名称、返回值和参数列表。

  如果派生类中的函数没有满足以上两个条件中的任意一个,编译器将产生编译错误。这样,我们可以确保在派生类中重写的函数与基类中的函数一致,避免了潜在的错误或误用。下面是一个示例:

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 
}
  1. capture list:捕获列表。捕获列表是lambda表达式的一部分,用于访问外部的变量。可以使用空括号[]表示不捕获任何变量,也可以使用方括号[变量名]来显式捕获一个或多个变量。捕获列表还可以使用值捕获和引用捕获,即使用[=]和[&]。
  2. parameters:参数列表。参数列表定义了传递给lambda表达式的参数,类似于函数的参数列表。可以省略参数类型,编译器会自动推导。
  3. mutable:在lambda表达式中,默认情况下是不允许修改被捕获的变量的。如果需要修改,则需要使用mutable关键字进行声明。
  4. return type:返回类型是可选的,如果省略,则编译器会自动推导返回类型。一般情况下,我们都是选择省略的,交给编译器自行推导。
  5. 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);// lamberauto 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),另一个版本可以指定返回类型。

  1. 无返回类型版本:

    • Fn&& fn:接受一个右值引用(模板的右值引用会产生引用折叠,也是万能引用),表示要绑定的可调用对象。
    • Args&&... args:接受一个可变数量的参数,表示要绑定给可调用对象的参数。
  2. 有返回类型版本:

    • 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的使用方法:

  1. 绑定普通函数:
    #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: 30return 0;
    }

      在这个例子中,bind函数绑定了函数func并将值10作为其第一个参数。然后,通过调用sum_func(20)来传递参数20给func,从而实现了延迟调用。最终的输出是 Sum: 30

  2. 绑定成员函数:
    #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: 42return 0;
    }

      在这个例子中,bind函数绑定了成员函数print,并指定了对象obj作为该成员函数的调用者。然后,通过调用print_func(42)来传递参数42给print函数,从而实现了延迟调用。最终的输出是 Number: 42

  3. 绑定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: 100return 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: 3return 0;
}

  在上面的示例中,我们定义了一个名为printNumbers的函数,该函数接受两个整数参数,并打印它们的值。然后,我们使用std::bind来交换参数的顺序,创建了一个新的可调用对象swappedPrintstd::bind的第一个参数是要适配的函数(或函数指针),然后是要传递给函数的参数。在这里,我们使用了两个特殊的占位符std::placeholders::_1std::placeholders::_2来表示原始函数的第一个和第二个参数。

  最后,我们通过调用swappedPrint来调用被适配的函数,传递了两个整数参数3和5。由于std::bind改变了参数的顺序,所以实际上会按照交换后的顺序将参数传递给原始函数,即先传递参数5,再传递参数3。因此,输出结果为:"a: 5, b: 3"。 

相关文章:

【C++】C++11新特性(下)

上篇文章&#xff08;C11的新特性&#xff08;上&#xff09;&#xff09;我们讲述了C11中的部分重要特性。本篇接着上篇文章进行讲解。本篇文章主要进行讲解&#xff1a;完美转发、新类的功能、可变参数模板、lambda 表达式、包装器。希望本篇文章会对你有所帮助。 文章目录 一…...

python内网环境安装第三方包

文章目录 一、问题二、解决方法三、代码实现 一、问题 内网安装第三方包的应用场景&#xff0c;一般是一些需要在没网的环境下进行开发的情况。这些环境一般仅支持本地局域网访问&#xff0c;所以只能在不下载任何第三方包的情况下艰难开发。 二、解决方法 将当前应用依赖的第…...

javaScipt

javaScipt 一、JavaScript简介二、javaScript基础1、输入输出语法2、变量3、常量4、数据类型4.1、数字型 number4.2、字符串类型 string4.3、布尔类型 boolean4.4、未定义类型 undefined4.5、null 空类型4.6、typeof 检测变量数据类型 5、数据类型转换5.1、隐式转换5.2、显示转…...

Linux(实操篇三)

Linux实操篇 Linux(实操篇三)1. 常用基本命令1.7 搜索查找类1.7.1 find查找文件或目录1.7.2 locate快速定位文件路径1.7.3 grep过滤查找及"|"管道符 1.8 压缩和解压类1.8.1 gzip/gunzip压缩1.8.2 zip/unzip压缩1.8.3 tar打包 1.9 磁盘查看和分区类1.9.1 du查看文件和…...

数学之美 — 1

为什么你会想和他人共享那些美丽的事物呢&#xff1f;因为这会让他&#xff08;她&#xff09;感到愉悦&#xff0c;也能让你在分享的过程中重新欣赏一次事物的美。 ——David Blackwell 1、感官之美&#xff0c;对于那些有规律的事物&#xff0c;你可以利用自己的视觉、触觉、…...

python中的global关键字

在Python中&#xff0c;global关键字用于在函数内部声明一个全局变量。默认情况下&#xff0c;函数内部的变量是局部变量&#xff0c;只能在函数内部访问。使用global关键字可以在函数内部创建或修改全局变量&#xff0c;使其在函数外部也可见和修改。 以下是使用global关键字…...

Matlab图像处理-幂次变换

幂次变换 如下图所示的幂次变换函数曲线图&#xff1a; 当γ <1时&#xff0c;效果和对数变换相似&#xff0c;放大暗处细节&#xff0c;压缩亮处细节&#xff0c;随着数值减少&#xff0c;效果越强。 当γ >1时&#xff0c;放大亮处细节&#xff0c;压缩暗处细节&…...

浏览器输入 URL 地址,访问主页的过程

分析&回答 浏览器解析域名&#xff1b;TCP建立连接&#xff1b;浏览器向服务器发送HTTP请求&#xff1b;服务器解析请求并返回HTTP报文&#xff1b;浏览器解析并渲染页面&#xff1b;断开连接。 反思&扩展 域名解析的流程 查找浏览器缓存——我们日常浏览网站时&am…...

每日一学————基本配置和管理

一、交换机的基本配置 配置enable口令、密码和主机名 Switch> (用户执行模式提示符) Switch>enable (进入特权模式) Switch# …...

解决 filezilla 连接服务器失败问题

问题描述&#xff1a; 开始一直用的 XFTP 后来&#xff0c;它变成收费软件了&#xff0c;所以使用filezilla 代替 XFTP 之前用的还好好的&#xff0c;今天突然就报错了&#xff1a;按要求输入相关字段&#xff0c;连接 连接失败&#xff01;&#xff01;&#xff01;o(╥﹏╥…...

如何使用Java进行机器学习?

在Java中进行机器学习&#xff0c;可以使用各种开源机器学习库和框架来实现。以下是一些常用的Java机器学习库&#xff1a; Weka&#xff1a;Weka 是一个非常流行的机器学习库&#xff0c;提供了大量的算法和工具&#xff0c;以及用于数据预处理、特征选择和可视化的功能。 De…...

springsecurity+oauth 分布式认证授权笔记总结12

一 springsecurity实现权限认证的笔记 1.1 springsecurity的作用 springsecurity两大核心功能是认证和授权&#xff0c;通过usernamepasswordAuthenticationFilter进行认证&#xff1b;通过filtersecurityintercepter进行授权。springsecurity其实多个filter过滤链进行过滤。…...

如何在职业生涯中取得成功

工作中让你有强烈情绪波动的事情 在我的工作经历中&#xff0c;有一次让我经历了强烈情绪波动的事件。我曾在一个高压的项目团队中工作&#xff0c;我们需要在极短的时间内完成一个复杂的客户项目。这个项目的截止日期非常紧迫&#xff0c;而项目的规模和要求也一直在不断增加…...

Hive-安装与配置(1)

&#x1f947;&#x1f947;【大数据学习记录篇】-持续更新中~&#x1f947;&#x1f947; 个人主页&#xff1a;beixi 本文章收录于专栏&#xff08;点击传送&#xff09;&#xff1a;【大数据学习】 &#x1f493;&#x1f493;持续更新中&#xff0c;感谢各位前辈朋友们支持…...

链表模拟栈

定义节点 class Node {var num: Int _var next: Node _def this(num: Int) {thisthis.num num}override def toString: String s"num[${this.num}]" }定义方法 class LinkStack {private var head new Node(0)def getHead: Node head//判断是否为空def isEmp…...

MySQL基础篇:数据库概述和部署

SQL 概述 SQL&#xff0c;一般发音为sequel&#xff0c;SQL的全称Structured Query Language)&#xff0c;SQL用来和数据库打交道&#xff0c;完成和数据库的通信&#xff0c;SQL是一套标准。但是每一个数据库都有自己的特性别的数据库没有,当使用这个数据库特性相关的功能,这…...

大数据面试题:MapReduce压缩方式

面试题来源&#xff1a; 《大数据面试题 V4.0》 大数据面试题V3.0&#xff0c;523道题&#xff0c;679页&#xff0c;46w字 可回答&#xff1a;1&#xff09;Hadoop常见的压缩算法有哪些&#xff1f; 问过的一些公司&#xff1a;网易云音乐(2022.11)&#xff0c;阿里(2020.…...

【ICer的脚本练习】“精通各种语言的hello world!“

系列的目录说明请见&#xff1a;ICer的脚本练习专栏介绍与全流程目录_尼德兰的喵的博客-CSDN博客 前言 这一节呢主要是检查一下Linux和win环境是不是能正常的支持咱们的脚本学习&#xff0c;所以来答应各种语言的hello world!&#xff0c;毕竟打印了就是学会了٩(๑❛ᴗ❛๑)۶…...

解决npm install报错: No module named gyp

今天运行一个以前vue项目&#xff0c;启动时报错如下&#xff1a; ERROR Failed to compile with 1 error上午10:19:33 error in ./src/App.vue?vue&typestyle&index0&langscss& Syntax Error: Error: Missing binding D:\javacode\Springboot-MiMall-RSA\V…...

Leetcode 面试题 17.01 不用加号的加法

设计一个函数把两个数字相加。不得使用 或者其他算术运算符。 示例: 输入: a 1, b 1 输出: 2 提示&#xff1a; a, b 均可能是负数或 0结果不会溢出 32 位整数 我的答案&#xff1a; 一、信息 1.设计一个函数把两个数相加 2.不得使用或者其他运算符 3.a,b均为负数或…...

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站&#xff0c;会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后&#xff0c;网站没有变化的情况。 不熟悉siteground主机的新手&#xff0c;遇到这个问题&#xff0c;就很抓狂&#xff0c;明明是哪都没操作错误&#x…...

Chapter03-Authentication vulnerabilities

文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...

TDengine 快速体验(Docker 镜像方式)

简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能&#xff0c;本节首先介绍如何通过 Docker 快速体验 TDengine&#xff0c;然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker&#xff0c;请使用 安装包的方式快…...

逻辑回归:给不确定性划界的分类大师

想象你是一名医生。面对患者的检查报告&#xff08;肿瘤大小、血液指标&#xff09;&#xff0c;你需要做出一个**决定性判断**&#xff1a;恶性还是良性&#xff1f;这种“非黑即白”的抉择&#xff0c;正是**逻辑回归&#xff08;Logistic Regression&#xff09;** 的战场&a…...

QMC5883L的驱动

简介 本篇文章的代码已经上传到了github上面&#xff0c;开源代码 作为一个电子罗盘模块&#xff0c;我们可以通过I2C从中获取偏航角yaw&#xff0c;相对于六轴陀螺仪的yaw&#xff0c;qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...

Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件

今天呢&#xff0c;博主的学习进度也是步入了Java Mybatis 框架&#xff0c;目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学&#xff0c;希望能对大家有所帮助&#xff0c;也特别欢迎大家指点不足之处&#xff0c;小生很乐意接受正确的建议&…...

Axios请求超时重发机制

Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式&#xff1a; 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...

计算机基础知识解析:从应用到架构的全面拆解

目录 前言 1、 计算机的应用领域&#xff1a;无处不在的数字助手 2、 计算机的进化史&#xff1a;从算盘到量子计算 3、计算机的分类&#xff1a;不止 “台式机和笔记本” 4、计算机的组件&#xff1a;硬件与软件的协同 4.1 硬件&#xff1a;五大核心部件 4.2 软件&#…...

【Post-process】【VBA】ETABS VBA FrameObj.GetNameList and write to EXCEL

ETABS API实战:导出框架元素数据到Excel 在结构工程师的日常工作中,经常需要从ETABS模型中提取框架元素信息进行后续分析。手动复制粘贴不仅耗时,还容易出错。今天我们来用简单的VBA代码实现自动化导出。 🎯 我们要实现什么? 一键点击,就能将ETABS中所有框架元素的基…...

全面解析数据库:从基础概念到前沿应用​

在数字化时代&#xff0c;数据已成为企业和社会发展的核心资产&#xff0c;而数据库作为存储、管理和处理数据的关键工具&#xff0c;在各个领域发挥着举足轻重的作用。从电商平台的商品信息管理&#xff0c;到社交网络的用户数据存储&#xff0c;再到金融行业的交易记录处理&a…...