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++语言核心特性解析-谢丙堃