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

news2024/11/24 10:41:06

 

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

  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);
	// 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),另一个版本可以指定返回类型。

  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: 30
    
        return 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: 42
    
        return 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: 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来交换参数的顺序,创建了一个新的可调用对象swappedPrintstd::bind的第一个参数是要适配的函数(或函数指针),然后是要传递给函数的参数。在这里,我们使用了两个特殊的占位符std::placeholders::_1std::placeholders::_2来表示原始函数的第一个和第二个参数。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/960016.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【整数列表求三的倍数】

问题描述: 给定一个从1到n的整数列表&#xff0c;从第一个数字开始计数&#xff0c;遇到3的倍数时&#xff0c;将该数从列表中删除&#xff0c;直至列表末尾。 在剩下的数字中&#xff0c;从第一个数字开始&#xff0c;继续之前的计数值&#xff0c;同样遇到3的倍数时&#xff…

插值法修正排斥能

( A, B )---3*30*2---( 1, 0 )( 0, 1 ) 让网络的输入只有3个节点&#xff0c;AB训练集各由5张二值化的图片组成&#xff0c;让A中有3个点&#xff0c;B全为0&#xff0c;排列组合A&#xff0c;统计迭代次数并排序。 其中的7组数据 差值结构 迭代次数 构造平均列 L E 0 1…

Jmeter的自动化测试实施方案

前言&#xff1a; Jmeter是目前最流行的一种测试工具&#xff0c;基于此工具我们搭建了一整套的自动化方案&#xff0c;包括了脚本添加配置、本地配置和运行、服务器配置等内容&#xff0c;完成了自动化测试闭环&#xff0c;通过这种快捷简便高效的方式&#xff0c;希望可以解…

【云原生之Docker实战】使用Docker部署flatnotes笔记工具

【云原生之Docker实战】使用Docker部署flatnotes笔记工具 一、flatnotes介绍1.1 flatnotes简介1.2 flatnotes特点 二、本地环境介绍2.1 本地环境规划2.2 本次实践介绍 三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本 四、下载flatnotes…

Python“梦寻”京东商品详情数据接口(含代码示例)

要通过京东的API获取商品详情数据&#xff0c;您可以使用京东开放平台提供的接口来实现。以下是一种使用Java编程语言实现的示例&#xff0c;展示如何通过京东开放平台API获取商品详情数据&#xff1a; 首先&#xff0c;确保您已注册成为京东开放平台的开发者&#xff0c;并创…

SourceTree安装教程

PS&#xff1a;SourceTree是一款流行的免费Git和Mercurial版本控制工具&#xff0c;由Atlassian开发和维护。它提供了一个直观且功能强大的图形用户界面&#xff0c;方便开发人员管理和浏览代码仓库 说白了&#xff0c;他就是一个可视化的git界面&#xff0c;还是非常好用的&am…

10.Redis 渐进式遍历

Redis 渐进式遍历 渐进式遍历scan 渐进式遍历 keys 命令一次性的把整个redis中所有的key都获取到&#xff0c;keys *但这个操作比较危险&#xff0c;可能会一下子得到太多的key,阻塞 redis 服务器。 通过渐进式遍历&#xff0c;就可以做到&#xff0c;既可以获取到所有的 key&…

wireshark 流量抓包例题

一、题目一(1.pcap) 题目要求&#xff1a; 1.黑客攻击的第一个受害主机的网卡IP地址 2.黑客对URL的哪一个参数实施了SQL注入 3.第一个受害主机网站数据库的表前缀&#xff08;加上下划线例如abc&#xff09; 4.第一个受害主机网站数据库的名字 看到题目SQL注入&#xff0c…

Python 案例实训教学,支持“教师-学生”双视角切换|ModelWhale 版本更新

学年伊始、辞旧迎新&#xff0c;金秋九月&#xff0c;ModelWhale 迎来新一轮的版本更新&#xff0c;持续优化你的使用体验。 本次更新中&#xff0c;ModelWhale 主要进行了以下功能迭代&#xff1a; • 新增 “教师-学生”双视角切换&#xff08;团队版✓&#xff09; • 新…

测试架构师必备技能 —— Nginx安装部署实战

Nginx("engine x")是一款是由俄罗斯的程序设计师Igor Sysoev所开发高性能的免费开源Web和 反向代理服务器&#xff0c;也是一个 IMAP/POP3/SMTP 代理服务器。在高并发访问的情况下&#xff0c;Nginx是Apache服务器不错的替代品。官网数据显示每秒TPS高达50W左右。本文…

从《孤注一掷》出发,聊聊 SSL 证书的重要性

你去看《孤注一掷》了吗&#xff1f;相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治&#xff0c;以及电信诈骗中系统性极强的诈骗技巧&#xff0c;引发了大量讨论。 图片来源于电影《孤注一掷》…

PostgreSQL 查询语句大全

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

多应用模式下,忽略项目的入口文件,重写Apache规则

多应用模式下&#xff0c;忽略项目的入口文件&#xff0c;重写Apache规则 首先&#xff0c;我的项目是具有两个应用&#xff0c;admin和index,同时给它们绑定了域名&#xff0c;但是每次访问时都需要加入项目的入口文件地址 index.php ,为了忽略这个入口文件&#xff0c;只能通…

Windows ROS2使用教程01-基础环境配置

Windows ROS2使用教程01-基础环境配置 背景及初衷 windows下ros2相关的文章比较少&#xff0c;官方文档针对windows的相关描述也有很多不明确的地方&#xff0c;现结合ros2官方文档关键点实操记录&#xff0c;希望对大家有所帮助&#xff1b;共勉&#xff01; ROS2环境配置 …

现代化畜牧业行业分析 - 商品猪养殖

改革开放以来&#xff0c;中国畜牧业生产基础条件不断改善、生产方式快速转变&#xff0c;畜牧业综合生产能力和保障市场有效供应能力不断加强。中国肉类、禽蛋产量均居世界第一位&#xff0c;奶类产量居世界第三位。随着产量的增长&#xff0c;中国人均畜产品占有量也持续上升…

手机电脑scoket通信 手机软件 APP inventor 服务端程序python

python scoket 通信 再帮助同学坐课题的时候接触到了scoket通信&#xff0c;了解到这应该是基层网络通信的原理&#xff0c;于是就导出搜索了一下相关的资料&#xff0c;简单来说scoket通信就是&#xff0c;可以让不同设备在同一个网络环境的条件下&#xff0c;可以实现相互通…

解读bl616的startup.S文件

startup.S是bl616的启动文件&#xff0c;以汇编格式存在。这就导致对需要看懂此文件的人要level高一些了&#xff0c;再加上汇编竟然是risc-v的&#xff0c;而不是arm的&#xff0c;导致本人还要恶补一下risc-v的汇编指令和risc-v的寄存器。 这里推荐一下比较的介绍risc-v架构…

百度云对象存储的图片转webp不展示图片的解决方案

前言 百度云对象存储的图片&#xff0c;加上图片处理的参数后&#xff08;像这种转webp格式的请求https://xxxxxxxx.cn/xxxxxx.jpg?x-bce-processimage/format,f_webp&#xff09;&#xff0c;由于图片过大&#xff0c;导致请求超时了。提交了工单咨询后解决了&#xff0c;特…

python评分卡模型

信用风险计量模型可以包括跟个人信用评级&#xff0c;企业信用评级和国家信用评级。人信用评级有一系列评级模型组成&#xff0c;常见是A卡&#xff08;申请评分卡&#xff09;、B卡&#xff08;行为模型&#xff09;、C卡&#xff08;催收模型&#xff09;和F卡&#xff08;反…

百度抓取香港服务器抓取超时是什么情况?

​ 网络延迟导致抓取超时 网络延迟是指从发送请求到接收响应之间的时间延迟。如果网络延迟过高&#xff0c;服务器可能无法及时响应请求&#xff0c;导致超时。在香港服务器上抓取数据时&#xff0c;如果网络延迟过高&#xff0c;可能会出现抓取超时的情况。 服务器负载过高可能…