通过for_each源码分析函数对象本质

前言

本文中将通过STL中的for_each算法为例来深度分析函数对象的本质,以及如何通过分析STL源码来学习STL中的容器、迭代器和算法。


提示:下面将通过编程实战手把手教学如何通过分析源码来学习算法并写出自己的函数对象。

4.1 搭建一个测试框架

使用STL中的算法要包含头文件,下面搭建一个测试案例,创建一个int型的vector数组,并装入10个随机数,然后自己实现一个遍历容器的函数,对定义好的容器进行遍历打印。

代码如下:

cpp 复制代码
#include <iostream>
using namespace std;

#include <vector>
#include <algorithm>

void print_vector_int(vector<int>& v)
{
	//for(vector<int>::iterator it = v.begin(); it != v.end(); it++)
	//{
	//	cout << *it << " ";
	//}
	for (unsigned int i = 0; i < v.size(); i++)
	{
		cout << v.at(i) << " ";
	}
	cout << endl;
}

int main()
{
	vector<int> V;
	for (int i = 0; i < 10; i++)
	{
		V.push_back(rand());
	}
	print_vector_int(V);
    system("pause");
	return 0;
}

4.2 分析for_each源码

4.2.1 分析for_each的函数参数和返回值

一个函数最重要的就是他的参数和返回值,分析函数的参数和返回值是深刻理解这个函数功能的重要前提。我们可以通过VS的转到定义功能来查看for_each函数的原型,代码如下:

cpp 复制代码
template <class _InIt, class _Fn>
	_Fn for_each(_InIt _First, _InIt _Last, _Fn _Func) { // perform function for each element [_First, _Last)
		_Adl_verify_range(_First, _Last); 
		auto _UFirst      = _Get_unwrapped(_First); 
		const auto _ULast = _Get_unwrapped(_Last); 
		for (; _UFirst != _ULast; ++_UFirst) { 
			_Func(*_UFirst); 
		}

		return _Func; //返回函数对象
	}

通过源码可以看出,这个函数有三个参数,第一个第二个参数应该是一个迭代器,第三个参数是一个函数对象(根据template后面的类型说明可以看出)。那么我们应该先定义一个函数对象,然后传入for_each实现功能。

既然要定义函数对象,根据我们前一篇文章的分析,函数对象就是一个重载了函数调用操作符()的类,首先我们应该定义一个类,并在类中实现函数调用操作符()的重载,这就涉及到我们的重载函数的接口应该是什么样子的,包括返回值、参数。根据函数重载的知识我们知道,返回值不是判断函数重载的标准,函数重载是根据函数参数的类型、个数、顺序等来决定的。也就是说函数重载的判断标准只与参数有关,因此,我们在重载函数调用操作符时应该重点关注函数参数。

我们要知道,类中重载的函数调用操作符()是在for_each中使用的,所以,要想知道重载函数的原型,我们应该去for_each源码中去分析,我们看for_each源码的return前一句

cpp 复制代码
_Func(*_UFirst);

for_each函数内部把一个*变量传给了函数对象,由此可见,我们重载的函数调用操作符函数应该只有一个参数。下面在分析这个参数的类型,我们继续看源码

cpp 复制代码
auto _UFirst      = _Get_unwrapped(_First); 
const auto _ULast = _Get_unwrapped(_Last);

从这里可以看出,_UFirst应该是一个指针,它指向了迭代器_First的位置,那么*_UFirst就是对指针解引用,他应该是容器中的元素(迭代器所指向的元素),也就是说我们需要重载的函数的参数类型是容器的元素的类型。这样我们就确定好了重载函数的参数。

通过for_each源码分析可知,在for_each内部并没有用到_Func(也就是函数对象)的返回值,我们可以把它定义为void类型,这样我们需要重载的函数调用操作符的函数接口就有了。

4.2.2 定义函数对象

根据重载函数接口

cpp 复制代码
void operator()(_Type t); //_Type和容器元素类型有关

我们可以定义一个类,并重载括号操作符,实现对容器元素+1的操作,另外在遍历容器的时候记录容器元素个数

cpp 复制代码
template<typename _MyType>
class MySort
{
public:
	MySort()
	{
		this->m_count = 0;
	}
public:
	void operator()(_MyType& t) //怎么确定这个函数对象的参数呢?
	{
		t++;
		this->m_count++;
	}
public:
	int get_count()
	{
		return m_count;
	}
private:
	int m_count; //记录容器中元素个数
};

这样,使用for_each的时候,容器中每个变量都会进入这个仿函数,通过私有属性m_count就可以记录总共调用了仿函数多少次,也就是容器中元素的个数。

这里有一点要注意,就是为什么要自定义一个无参构造函数并将私有属性m_count置为0,如果我们不这么做的话,当我们使用类MySort定义对象的时候,编译会报错。

cpp 复制代码
MySort m_sort1;

//错误	C4700	使用了未初始化的局部变量"m_sort1"	

这样我们便可以在主函数中添加以下代码进行测试

cpp 复制代码
for_each(V.begin(), V.end(), MySort<int>()); //使用匿名函数对象
print_vector_int(V);

编译运行,可以看到功能已经实现。

4.2.3 for_each函数的返回值

在前面定义类的时候我们定义了一个私有属性m_count来记录容器元素个数,那么我们继续添加以下代码进行测试

cpp 复制代码
MySort<int> m_sort1; //MySort有私有变量m_count,所以需要提供构造函数初始化m_count
for_each(V.begin(), V.end(), m_sort1);
cout << "计数:" << m_sort1.get_count() << endl;
print_vector_int(V);

并编译运行,查看打印结果

我们看到计数值显示0,这和我们定义的10个元素的容器不符,那么问题出现在哪呢,我们继续看for_each的接口原型

cpp 复制代码
_Fn for_each(_InIt _First, _InIt _Last, _Fn _Func)

问题就出现在这里,函数参数_Func是一个_Fn类型的元素,我们传进的实参m_sort1也是一个元素,但是实参和形参是两个不同的元素,我们把实参m_sort1复制给了形参_Func,在for_each函数内部对形参_Func的属性m_count进行了++运算,但是这和实参m_sort1毫无关系,实参m_sort1的m_count还是0。

注:实参和形参是两个完全不同的变量,在函数调用的时候只是把实参复制给了形参,实参还是实参,形参就是形参,他们两个没有任何关系。要想通过函数参数(形参)改变实参,就要使用引用或者指针(指针做函数参数间接修改实参)。

那么我们怎么使用m_count这个私有属性来打印容器元素个数呢,继续看源码,看for_each函数的返回值

cpp 复制代码
return _Func;

for_each的返回值是一个_Fn 类型的_Func,也是函数的参数_Func。也就是说,形参通过返回值的形式被for_each函数返回了出来,那么我们便可以这么用,继续添加如下代码

cpp 复制代码
MySort<int> m_sort2; 
m_sort2 = for_each(V.begin(), V.end(), m_sort2);
cout << "计数:" << m_sort2.get_count() << endl;
print_vector_int(V);

编译运行

我们看到,计数为10,与容器内元素个数相同。

这里应注意,我们用m_sort2来接for_each的返回值时,因为for_each返回的是一个元素,他会把这个元素复制给m_sort2并调用拷贝构造函数,因为我们的类中没有指针,不涉及深拷贝浅拷贝问题,所以不必写深拷贝构造函数,使用默认的浅拷贝构造函数即可。但是,如果类中有指针等涉及深拷贝问题的情况时,一定要手动实现深拷贝构造函数。 函数对象实参传递给for_each的形参时也会调用拷贝构造函数,这里可以参考拷贝构造函数调用时机(实参初始化形参)。

4.2.4 for_each源码浅析

最后附上本人对for_each源码的理解,解析在代码注释中。

cpp 复制代码
template <class _InIt, class _Fn>
_Fn for_each(_InIt _First, _InIt _Last, _Fn _Func) { //函数对象实参传递给形参调用拷贝构造函数
// perform function for each element [_First, _Last)
	_Adl_verify_range(_First, _Last); //范围:容器迭代器 begin 到 end
	auto _UFirst      = _Get_unwrapped(_First); //指针_UFirst指向迭代器_First的位置
	const auto _ULast = _Get_unwrapped(_Last); //指针_ULast迭代器_Last的位置
	for (; _UFirst != _ULast; ++_UFirst) { //遍历容器
		_Func(*_UFirst); //把容器元素逐个放入函数_Func作为其函数参数
	}

	return _Func; //返回函数对象
}
相关推荐
攸攸太上7 分钟前
Spring Gateway学习
java·后端·学习·spring·微服务·gateway
罗曼蒂克在消亡23 分钟前
graphql--快速了解graphql特点
后端·graphql
潘多编程25 分钟前
Spring Boot与GraphQL:现代化API设计
spring boot·后端·graphql
大神薯条老师1 小时前
Python从入门到高手4.3节-掌握跳转控制语句
后端·爬虫·python·深度学习·机器学习·数据分析
2401_857622662 小时前
Spring Boot新闻推荐系统:性能优化策略
java·spring boot·后端
AskHarries2 小时前
如何优雅的处理NPE问题?
java·spring boot·后端
计算机学姐3 小时前
基于SpringBoot+Vue的高校运动会管理系统
java·vue.js·spring boot·后端·mysql·intellij-idea·mybatis
猿java4 小时前
Cookie和Session的区别
java·后端·面试
程序员陆通4 小时前
Spring Boot RESTful API开发教程
spring boot·后端·restful
无理 Java4 小时前
【技术详解】SpringMVC框架全面解析:从入门到精通(SpringMVC)
java·后端·spring·面试·mvc·框架·springmvc