前言
在刷 LeetCode 时,遇到一个问题,我想用 Lambda 表达式自定义排序。
用在 std::sort 上,一行代码搞定,丝滑流畅;
用在 std::priority_queue 上,直接报错,查了查,学着用 decltype(cmp) 解决问题。
为什么会有这种巨大的差异?
带着这个疑问,本文将深入底层,彻底揭开 sort、priority_queue、Lambda 与 decltype 之间错综复杂的“猫腻”。
第一阶段:C++98/03 —— 远古时代(没有 Lambda 的黑暗岁月)
这一时期,std::sort 和 std::priority_queue 作为 STL 的元老就已经存在了。但在那个没有 Lambda 表达式、没有 auto、没有 decltype 的“三无”年代,想要自定义一个简单的排序规则(比如让整数从大到小排,或者给结构体按某个字段排),简直是一场“手速”的考验。
当时的痛点:
你不能像现在这样随手写个匿名函数,你必须显式地写一个完整的 “仿函数” (Functor) 结构体。这意味着哪怕只是为了一个非常简单的 a > b 逻辑,你也得专门在 main 函数外面定义一个类。
1. 代码实录:那个年代的“标准写法”
假设我们要对一组整数进行降序排序
// C++98 写法:必须先定义一个“仿函数”结构体
// 这是一个【图纸】(Type)
struct MyCompare {
// 重载 () 运算符,让结构体对象能像函数一样被调用
bool operator()(int a, int b) {
return a > b;
}
};
// ... 在 main 函数中 ...
std::vector<int> v = {1, 3, 2};
// 场景 A:std::sort(函数模板)
// 注意:这里传的是 MyCompare() —— 带括号,这是一个【临时对象】
std::sort(v.begin(), v.end(), MyCompare());
// 场景 B:std::priority_queue(类模板)
// 注意:这里传的是 MyCompare —— 不带括号,这是一个【类型】
std::priority_queue<int, std::vector<int>, MyCompare> pq;
2. 深度解析:为什么一个带括号,一个不带?
这是 C++ 初学者最容易迷惑的地方,也是理解 decltype 诞生原因的关键。我们需要引入一个核心比喻:“图纸 (Type)” 与 “房子 (Object)”。
-
MyCompare(不带括号):这是图纸(Type)。它告诉编译器这个结构体长什么样,占多少内存,有什么功能。图纸本身不能干活。 -
MyCompare()(带括号):这是调用构造函数,根据图纸造出来的房子(Object)。它是实实在在的内存实体,可以被调用,可以干活。
(1) std::sort —— 包工头找工人
std::sort 是一个函数模板。函数的参数列表 (...) 就像是包工头的招工名单,他需要的是能实实在在干活的“工人”。
-
如果你传
MyCompare(图纸):编译器会报错,因为函数参数不能接收一个类型。包工头会说:“你给我一张纸没用,我要一个能比较大小的实例。” -
所以必须传
MyCompare():这表示现场构造一个临时对象(工人),扔进sort函数里。sort内部会在循环时真正调用这个对象:cmp_obj(a, b)。
(2) std::priority_queue —— 设计师画蓝图
std::priority_queue 是一个类模板。尖括号 <...> 里是在定义这个容器的内存布局,这就像是在设计一个仓库的蓝图。
-
尖括号
< >里的对话是这样的:-
编译器:“这个优先队列里存什么原料?”
int(类型)。 -
编译器:“底层用什么容器装?”
std::vector<int>(类型)。 -
编译器:“以后谁负责管理顺序?请把他的简历(类型)给我,我好在仓库里给他预留工位。”
-
你:“是
MyCompare这种类型的人。”这里必须填类型!
-
-
如果在这里写
MyCompare()(对象):编译器会直接报错:“我要的是**图纸(Type)来规划内存,你塞给我一个活人(Object)**算怎么回事?”
3. 时代的局限性
虽然这种写法在今天看来非常繁琐(写了五六行代码只为了改个大于号),但在当时,它的逻辑是非常严谨且一致的:
-
凡是函数传参
(...),就给对象。 -
凡是模板定义
<...>,就给类型。
这种“死板”的严谨,在当时相安无事。但它埋下了一个隐患:如果有一天,我们有了一种“只有对象,没有名字(无法写出图纸名称)”的东西(比如后来的 Lambda),这套规则就会瞬间崩塌。
这也正是 C++11 引入 decltype 的根本原因——为了在只有“房子”的情况下,反向推导出“图纸”。
第二阶段:C++11 —— 变革时代(Lambda 与 decltype 诞生)
这是 C++ 历史上巨大的变革。
-
新特性 1:Lambda 表达式
[](){}诞生。匿名函数,随手即写。 -
新特性 2:decltype 诞生。可以推导表达式的类型。
-
新特性 3:auto 诞生。
矛盾爆发点:
Lambda 是匿名的(编译器在幕后生成了一个你不知道名字的类)。
1. std::sort 的“无缝衔接”
std::sort 是函数模板。C++98 就支持函数参数类型推导。
auto cmp = [](int a, int b) { return a > b; };
// 编译器:你传了个 cmp 对象给我,我自动推导它的类型,搞定!
std::sort(v.begin(), v.end(), cmp);
结果:Lambda 在 sort 里混得风生水起。
2. std::priority_queue 的“至暗时刻”
std::priority_queue 是类模板。在 C++11 中,类模板不支持通过构造函数参数推导类型。
必须在尖括号 < > 里显式写出类型,可是 Lambda 没有名字啊!
救世主 decltype 登场:
既然写不出名字,就用 decltype 提取类型。
// C++11 标准写法
auto cmp = [](int a, int b) { return a > b; };
std::priority_queue<
int,
std::vector<int>,
decltype(cmp) // <--- 必须用 decltype 提取类型填坑
> pq(cmp); // <--- 必须传入 cmp 对象初始化
评价:这就是“麻烦”的根源。这是 C++11 时代的妥协方案。
第三阶段:C++14 —— 完善时代(Lambda 增强)
C++14 对 C++11 做了小修小补。
-
泛型 Lambda:
[](auto a, auto b) { ... }。 -
decltype(auto):主要用于函数返回值推导。
对 priority_queue 的影响:
几乎没有。
你依然需要使用 decltype 大法来伺候 priority_queue。繁琐的语法依然存在。
第四阶段:C++17 —— 现代文明(CTAD 降临)
这是刷题党最幸福的时代。
-
新特性:CTAD (Class Template Argument Deduction,类模板实参推导)。
什么意思?
编译器终于进化了!它现在看类模板的构造函数参数,也能像函数模板一样自动推导类型了!
priority_queue 的“大翻身”:
// C++17 标准写法
auto cmp = [](int a, int b) { return a > b; };
// 见证奇迹的时刻:
// 不需要 <int, vector<int>, decltype(cmp)> 了!
// 编译器看你传了 cmp,自动推导出它是比较器类型!
std::priority_queue pq(cmp);
评价:如果你现在的 LeetCode 编译器选的是 C++17 或 C++20,你完全可以把 decltype 扔进垃圾桶了。但是该写法依赖于标准库实现的推导指引,部分旧编译器可能不工作。
第五阶段:C++20 —— 极致简化(无状态 Lambda 默认构造)
C++20 对 Lambda 又做了一个微小的但重要的改动:
无捕获的 Lambda(Stateless Lambda)有了默认构造函数。
在 C++11/14/17 中,即使你用 decltype 填了坑,你还必须在构造函数里传 cmp 对象:pq(cmp)。
但在 C++20 中:
// C++20 写法
auto cmp = [](int a, int b) { return a > b; };
// 注意:这里没传构造参数 pq(cmp)!
// 只要 cmp 是无捕获的 (Stateless),它就可以默认构造。
std::priority_queue<int, std::vector<int>, decltype(cmp)> pq;
(注:但这反而让 decltype 写法又稍微简洁了一点点,不过依然不如 C++17 的 CTAD 方便)
总结:一张表看懂演进
| 特性 / 版本 | C++98/03 | C++11 | C++17 |
| 自定义排序工具 | struct 仿函数 | Lambda 表达式 | Lambda 表达式 |
| 获取类型工具 | 无 (手写类型名) | decltype | decltype (但不强制用了) |
| std::sort | 传对象 (自动推导) | 传 Lambda 对象 (自动推导) | 传 Lambda 对象 (自动推导) |
| std::priority_queue |
显式写类型
|
用 decltype
|
CTAD 自动推导
|
| 开发体验 |
繁琐 (都痛) |
割裂 (sort 爽,pq 痛) |
统一 (都爽) |
-
sort是函数,一直很聪明。 -
priority_queue是类,直到 C++17 才变得聪明。 -
decltype是 C++11 为连接“必须写类型”的旧时代与“拥有匿名对象”的新时代,架起的一座桥梁。
270

被折叠的 条评论
为什么被折叠?



