类中的成员函数加static修饰与否的区别(decltype视角探究)

0️⃣、小白入手

decltype的基本认识及用法,请各位看官移步笔者的另一篇博文:浅析decltype一些有趣(实用)的用法

一、问题由来

        为什么会有这样一个问题呢?笔者在完成LeetCode #1224 设计力扣排行榜这一系统设计问题时,偶然发现其中一个成员函数加static修饰和不加static修饰时,使用decltype来解析其函数类型其效果是不一样的

        而正是因为两者的不一样,若将decltype解析出来的结果作为某些容器初始化时的参数,则有可能出现报错行为。为了方便说明问题以及做实验验证,笔者简化后的代码列在下面:

#include <iostream>
#include <set>
#include <unordered_map>

using namespace std;

class LeaderBoard
{
private:
	using Player = pair<int, int>;  //id, score

	bool cmp(const Player& p1, const Player& p2)
	{
		if (p1.second == p2.second) return p1.first > p2.first;

		else return p1.second > p2.second;
	}

	unordered_map<int, Player> record;     //记录玩家是否出现过
	set<Player, decltype(&cmp)> players;  //对玩家进行排序(按序号和成绩)

public:
	LeaderBoard() : players(cmp) {}
	~LeaderBoard()
	{
		record.clear();
		players.clear();
	}

	void addScore(int playerId, int score)  //O(logn)
	{
		Player p{ playerId, score };

		if (record.count(playerId))
		{
			p.second += record[playerId].second;
			players.erase(record[playerId]);
		}

		//更新记录
		record[playerId] = p;
		players.emplace(p);
	}

	int top(int k)   //O(1)
	{
		int sum = 0;
		for (auto it = players.begin(); it != players.end() && k > 0; ++it, --k)
		{
			sum += it->second;
		}

		return sum;
	}

	void reset(int playerId)  //O(1)
	{
		if (record.count(playerId))
		{
			players.erase(record[playerId]);
			record.erase(playerId);
		}
	}
};

int main()
{
	LeaderBoard lb;

	lb.addScore(1, 20);
	
	return 0;
}

[程序编译结果]

         由于set容器的模板参数中传入了自定义的比较函数类型,故在构造函数中也该对set容器进行初始化(圆括号中填入cmp,即比较函数的入口地址)。

        然而,我们却惊奇的发现这样对players传参初始化会得到一个编译错误,很容易按照编译器的错误提示进一步修改为如下的代码,但编译器依然报错:

         正当我们抓耳挠腮之际,笔者悄悄将cmp这个成员函数加上的static修饰,然后,神奇的事情就发生了,这个编译器错误竟然不见了!!! 这背后发生了什么??

二、深入分析1(从函数定义出发)

        从上面的改动可以看出,问题似乎在于普通成员函数与静态成员函数的区别,那我们先来回顾一下这两者的区别有哪些:

static成员函数普通成员函数
所有对象共享
隐含this指针
访问普通成员变量(函数)
访问静态成员变量(函数)
通过类名直接调用
通过对象名直接调用

        static成员函数最大的特点在于其不隶属于某个对象,而是隶属于整个类。为了说明这普通成员函数与static成员函数的具体区别,笔者特意准备了一个小demo来说明上面的这些结论:

#include <iostream>

using namespace std;

class Test
{
public:
	int func(int i) { return i; }

	static int func1(int i) { return i; }
};

int main()
{
	int(Test:: *pF)(int) = &Test::func;  //创建并初始化一个指向普通成员函数的指针

	Test t;

	cout << (t.*pF)(100) << endl;    //对于普通成员函数指针而言, 必须通过类的实例化对象才能使用, 即直接写pF(100)是会报错的

	int(*pF1)(int) = &Test::func1; //创建并初始化一个指向static成员函数的指针

	cout << pF1(200) << endl;    //对于static成员函数指针而言, 可直接根据指针名来调用static成员函数

	return 0;
}

【程序结果】

         这种声明成员函数指针的写法比较少见,部分读者或许没有见过,但就语义来看还是比较好理解的。同时,从这个例子也可以看出,static成员函数就是隶属于整个类的(首先,声明并初始化pF1时无需加上Test::修饰;其次,通过pF1来调用static成员函数时也无需通过具体的Test类对象)。这是否有种静态成员函数好比是“全局函数”的感觉呢?

三、深入分析2(借用boost库的类型推导功能)

        看到这可能还是有不少读者与我一样,对上面的分析依然朦朦胧胧的,对此笔者想从函数类型可视化这一角度出发,来彻底地解决读者们的疑惑。

        对原有的代码进行了适当的改造,读者们可把注意力放在cmp、cmp1、cmp2这三个函数上,同时代码中对cmp1的函数类型做了typedef别名定义为CMP1。

#include <iostream>
#include <boost/type_index.hpp>

using namespace std;

int cmp2(int i, int j)
{
	return i + j;
}

class LeaderBoard
{
private:
	using Player = pair<int, int>;  //id, score

	static bool cmp(const Player& p1, const Player& p2)
	{
		if (p1.second == p2.second) return p1.first > p2.first;

		else return p1.second > p2.second;
	}

	bool cmp1(const Player& p1, const Player& p2)
	{
		if (p1.second == p2.second) return p1.first > p2.first;

		else return p1.second > p2.second;
	}

	typedef decltype(&LeaderBoard::cmp1) CMP1;

public:
	void printType()
	{
		using boost::typeindex::type_id_with_cvr;
		cout << "CMP1 = " << type_id_with_cvr<CMP1>().pretty_name() << endl;
		cout << "cmp = " << type_id_with_cvr<decltype(&cmp)>().pretty_name() << endl;
		cout << "cmp1 = " << type_id_with_cvr<decltype(&LeaderBoard::cmp1)>().pretty_name() << endl;
		cout << "cmp2 = " << type_id_with_cvr<decltype(&cmp2)>().pretty_name() << endl;
	}
};

int main()
{
	LeaderBoard lb;

	lb.printType();

	return 0;
}

【程序结果及分析】

         从结果中可以清晰地看到:CMP1和cmp1的函数类型相同,均为LeaderBoard::*型的指针,而cmp(对应static成员函数)和cmp2(对应普通全局函数)两者的函数类型均属于同一类,即全局性的函数指针类型,没有任何类限定符修饰

        这也就正好应证了“深入分析1”处所做的工作,static成员函数真就好比有种“全局函数”的感觉,但不能简单粗暴理解为static成员函数就是普通的全局函数,两者实际上还是有差别的

        下次碰到类似的使用场景时,读者可自行选择是使用static成员函数还是普通全局函数,来作为decltype的推导参数。

【参阅资料】

狄泰软件学院-C++深度解析教程

C++新经典-王建伟

现代C++语言核心特性解析-谢丙堃

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值