“比较器怎么传不对”“为什么我的priority_queue会报错编译不过”——这些问题十有八九都出在同一个根源上没搞明白类中的operator()重载到底在干什么以及STL里的谓词是以什么机制工作的。我当初第一次接触priority_queue时也栽在这上面。当时想把队列从“默认大顶堆”改成“小顶堆”照着网上的例子写了个仿函数结果一脸懵地对着编译报错研究了半天。这篇文章就把这条线一次讲透operator()重载、谓词、优先级队列构建三者的关系是什么真正写代码时要注意什么我把踩过的坑和验证过的写法都放出来。适合谁看刚学完STL基础、准备在实际项目里用优先队列处理任务调度或者读别人源码时被自定义比较器搞晕的C开发者。重点在于搞清楚“为什么这样写就对了”而不是只抄一行代码。1. 类中的operator()重载函数对象的“伪装术”1.1 一个看起来不像调用的“调用”在C里你见过这样的写法吗class MyGreater { public: bool operator()(int a, int b) const { return a b; } }; int main() { MyGreater cmp; bool result cmp(3, 5); // 看起来像在调用函数 // result 为 false因为 3 5 不成立 }关键在cmp(3, 5)这一行。cmp是一个对象不是函数名但它可以被“调用”。这是因为我们在类里重载了operator()也就是函数调用运算符。当你对一个对象使用对象名(参数...)的语法时编译器会把它翻译成对象.operator()(参数...)也就是调用这个对象的成员函数。这有什么用最直接的价值对象是有状态的。普通函数想要“记住”某些数据得用全局变量、静态变量一团糟。而函数对象可以像普通对象一样拥有成员变量你可以在构造时传入参数也可以在调用过程中修改和查询状态。这一点是普通函数指针做不到的。1.2 为什么要专门重载operator()而不是普通成员函数有同学会问我定义一个普通的成员函数bool compare(int a, int b)用cmp.compare(3, 5)调用不也一样吗为什么非要重载operator()核心原因有三个第一语法统一。STL算法和容器比如std::sort、priority_queue在模板参数中接收的是“可调用对象”。如果要求你必须传成员函数指针接口会变得异常复杂。sort的第三个参数、priority_queue的第三个模板参数、find_if的第三个参数它们都期望一个“能直接用函数(参数)语法调用”的东西。函数对象重载operator()后形态与函数完全一致STL不需要区分你传的到底是个函数指针还是对象在模板内部统一写cmp(a, b)就完事了。第二内联优化。编译器看到cmp(3, 5)被翻译成cmp.operator()(3, 5)而这个operator()是在类定义体内部实现的天然具备内联条件。相比传函数指针函数对象的调用往往可以被完全展开优化纯粹从性能角度来说函数对象通常更快。对于priority_queue里每次push/pop都要做比较的场景这省下的开销会在海量数据操作时体现出来。第三便于携带配置和状态。比如你想构造一个“按字符串长度从小到大排序”的比较器并且长度阈值可以动态配置。用函数对象你可以在构造函数里保存阈值每个对象有自己独立的配置用普通函数你得引入全局变量或额外参数可维护性断崖下跌。1.3 operator()的参数与返回值约定在实际写仿函数函数对象时有几个约定俗称的规则通常定义成const成员函数表示这个比较操作不会改变对象内部状态。这样const对象也能调用。返回值通常是bool表示一种判断关系。参数个数不限。一元谓词接收一个参数二元谓词接收两个参数。三者分别用在不同场景下。我在项目里习惯这样定义一个比较器struct Task { int priority; int deadline; std::string name; }; struct TaskHigherPriority { bool operator()(const Task a, const Task b) const { if (a.priority ! b.priority) return a.priority b.priority; // 优先级大的在前 return a.deadline b.deadline; // 优先级相同截止时间早的在前 } };这里TaskHigherPriority就是一个二元谓词接收到两个Task并返回bool同时它又是一个函数对象。它定义的“排序规则”是优先级更高者排前面如果优先级相等截止时间更早的排前面。后面我们会用它在priority_queue里做自定义优先级。注意仿函数的核心价值在于在类内部可以持有任何状态和逻辑同时对外暴露统一的“调用”接口。想明白这一点后面理解priority_queue的模板参数就有基础了。2. STL中的谓词一颗“返回bool的函数”2.1 谓词到底是什么玩意儿“谓词”这个词听上去高大上实际上在STL里它就是一个返回bool值的可调用对象。用我自己的话说谓词是一台“判断器”。给它输入它告诉你“是或否”。按照接受参数个数STL谓词分为两种一元谓词接收一个参数比如“判断某个数是否为偶数”。典型使用场景是std::find_if、std::count_if、std::remove_if。二元谓词接收两个参数比如“a是否大于b”。典型使用场景是std::sort的比较器、priority_queue 的比较器、std::unique相邻去重等。无论在哪种场景谓词的本质都是对“关系”或“属性”的抽象。你给STL的算法传一个谓词等于告诉它“按这个标准去判断”。2.2 函数指针充当谓词最简单但别受限最直观的做法是传普通函数指针。比如bool isEven(int x) { return x % 2 0; } std::vectorint nums {1, 2, 3, 4, 5, 6}; auto it std::find_if(nums.begin(), nums.end(), isEven); // it 指向第一个偶数 2这没问题函数指针就是一种可调用对象。但它有两个明显短板不能携带额外配置如果你还想传递一个“区间范围”或“阈值”函数指针就服务不了。2.3 lambda表达式写谓词最爽的方式C11引入lambda之后很多地方我都直接拿lambda当谓词用。本质上lambda表达式在编译后会生成一个匿名的仿函数类它同样有operator()只是我们不用手动写出类定义。比如int threshold 10; auto isBiggerThanThreshold [threshold](int x) { return x threshold; }; std::vectorint nums {5, 9, 12, 3}; int count std::count_if(nums.begin(), nums.end(), isBiggerThanThreshold); // count 为 1只有12从这段代码可以看到lambda的捕获列表[threshold]对应仿函数的成员变量参数列表对应operator()的参数函数体对应operator()的函数体。lambda就是operator()重载的语法糖但心里明白这个本质对避开很多编译错误至关重要。2.4 谓词与排序、优先级的关系我们在写排序和优先队列时对谓词的语义要格外敏感。以std::sort为例默认使用小于比较结果是升序排列。你传的二元谓词comp(a, b)如果返回 true表示a在b前面是“可接受的顺序”。一个极其容易记混的点priority_queue的默认比较是std::lessT但生成的却是大顶堆即最大值在堆顶。这个“看似反直觉”的设计让无数人掉过坑。为什么因为priority_queue从容器尾部pop时为了保证堆顶是最大值需要采用相反的比较方向。更直接的理解是priority_queue的Compare语义不是“谁更大谁在前”而是“谁应该更靠近堆底”。std::less会认为小元素“应该更靠近堆底”剩下的自然是大元素在堆顶。这里我强烈建议的记法是在这个语境里暂时忘掉“小于、大于”的语言含义只记住priority_queue使用std::less得到大顶堆使用std::greater得到小顶堆。具体原因下面第3章展开。3. 优先级队列构建priority_queue的核心机制3.1 默认行为容器适配器与“堆”的组合std::priority_queue是一个容器适配器。它底层默认使用std::vector存储元素并通过std::make_heap、std::push_heap、std::pop_heap这三个堆算法维护元素顺序。所以priority_queue并不保证内部元素完全有序它只保证堆顶是当前优先级最高的元素。它的模板声明长这样来自标准库我按自己理解重写一遍template class T, class Container std::vectorT, class Compare std::lesstypename Container::value_type class priority_queue;三个模板参数分别代表T元素类型。Container底层容器默认vector。但你也可以换成std::dequeT只要容器支持random access iterator、push_back、pop_back、front操作即可。Compare比较器也就是二元谓词默认std::lessT。实际使用时你可能99%都用默认的vector凑合但了解 Container 可以替换会增加理论深度。比如任务系统里想“小内存优先”或者容量已知时自定义内存池容器甚至都可行。日常不需要深究。3.2 构建步骤从构造到push/pop构建一个优先队列非常简单#include iostream #include queue #include vector int main() { // 1. 默认大顶堆 std::priority_queueint pq1; pq1.push(10); pq1.push(30); pq1.push(20); std::cout pq1.top() std::endl; // 30 // 2. 通过迭代器范围构造 std::vectorint nums {5, 2, 8, 1}; std::priority_queueint pq2(nums.begin(), nums.end()); std::cout pq2.top() std::endl; // 8 // 3. 底层容器为deque std::priority_queueint, std::dequeint pq3; pq3.push(7); std::cout pq3.top() std::endl; // 7 }这里最重要的操作是top()它返回堆顶元素的const引用。如果你访问堆顶后要删除它得先pop()push一个元素内部会重新调整堆结构时间复杂度为 O(log N)。3.3 用std::greater构建小顶堆最经典的“反直觉”场景来了std::priority_queueint, std::vectorint, std::greaterint minHeap; minHeap.push(5); minHeap.push(1); minHeap.push(3); std::cout minHeap.top() std::endl; // 1最小值在堆顶注意第二个模板参数是std::vectorint第三个是std::greaterint。为什么“greater”反而得到的是小顶堆得回归堆算法本质。priority_queue内部往堆里塞元素时会调用Compare来比较父子关系。当使用std::greaterint时比较规则是“a b 返回 true”。堆调整算法希望父节点与子节点按Compare比较时父节点不应该“落后”于子节点。也就是说堆顶是“按Compare来看最大”的那个元素。用std::greater时哪个元素“更大”如果a比较大greater(a,b)返回true表示a顺序上“优于”b所以大的元素反而会沉到底部小的元素浮到顶部。Counter-intuitive吧但你只要记住结论std::less→ 大顶堆std::greater→ 小顶堆。为什么标准库这么设计因为priority_queue在设计上继承了std::less的默认行为less在make_heap/push_heap下构建的是“最大堆”这是历史原因和标准库的规范性约定。3.4 自定义类型构建优先级队列必须有Compare当你往priority_queue里放自定义类型Task时问题来了默认的std::lessTask怎么比较两个TaskTask没有重载编译器直接报错。此时你有两条路给Task重载operator让默认的std::lessTask可以工作。适合类型本身有“天然顺序”的场景。显式传入自定义Compare谓词这也是本文的核心之一。适合不想侵入类型定义或者有多个排序标准的场景。两条路各有优劣。重载operator代码最简短但会让类型“被一种顺序绑死”。使用自定义Compare更灵活但要写好仿函数或lambda。在实际项目里我更倾向于第二种因为任务系统的排序标准经常随着业务调整。举例我们的Task类不重载operator而是用一个仿函数struct Task { int priority; std::string name; }; struct TaskCmp { bool operator()(const Task a, const Task b) const { return a.priority b.priority; // 注意这里用的是 } }; // 那么堆顶是优先级最大的 std::priority_queueTask, std::vectorTask, TaskCmp taskQueue; taskQueue.push({3, task3}); taskQueue.push({7, task7}); taskQueue.push({1, task1}); std::cout taskQueue.top().name std::endl; // 输出 task7看到没虽然TaskCmp使用的是a.priority b.priority但因为priority_queue的默认行为是大到小堆顶传给它的比较器一般按“小于”语义去写堆顶就是“最大”的那个。此时仿函数里的“”反而对应大顶堆。这和默认的std::less一样。想实现“按priority小优先出队”你就把比较写成a.priority b.priority也就是用“更大”来表示“优先级更高”堆顶相反成了最小的。这彻底验证了之前那个要点对priority_queue来说Compare决定的是“谁排在堆底”而堆顶是“按Compare与所有元素都优”的那个直接按“谁优先级高谁出队”的直觉来写先把优先级的定义和比较器的方向弄清楚再选择或。我不希望你死记给你一个自洽推导堆算法要求全序关系。用Compare(a,b) true表示“a优先级低于b”。调用make_heap后堆顶是“优先级最高”的元素即对于任意堆顶X和堆内Y都有Compare(Y, X) false或Compare(X, Y) false也就是没有任何元素比X“更优”。因此写比较器时你定义了什么语义堆顶就是那个语义下“最不可能败下阵”的元素。默认less是“ab表示a更差”那堆顶就是“不小于任何人”的最大者。自定义Compare不太直观但它给了你完全控制权。这也是很多人学习STL优先级队列时花费时间最多的地方——一旦想通这个点基本就通关了。4. 实战让优先级队列听你的话——自定义Compare与仿函数4.1 需求场景任务调度器假设你在做一个任务调度模块需要维护一个待执行任务队列。任务有“优先级(int)”和“提交顺序(int序号)”。要求优先级高的任务先出队。如果优先级相同提交顺序早的先出队即 FIFO。你不能改Task类的定义那是别的模块的只能通过compare控制。那你的priority_queue可以这样配#include iostream #include queue #include vector #include string struct Task { int priority; int seq; // 提交序号越小越早 std::string name; }; struct TaskCompare { bool operator()(const Task a, const Task b) const { if (a.priority ! b.priority) return a.priority b.priority; // 高优先级在前 写 return a.seq b.seq; // 同优先级seq小在前 写 } }; int main() { std::priority_queueTask, std::vectorTask, TaskCompare queue; queue.push({2, 1, middle-task}); queue.push({5, 2, high-task}); queue.push({2, 3, early-middle-task}); while (!queue.empty()) { Task t queue.top(); std::cout t.name (priority t.priority , seq t.seq )\n; queue.pop(); } }输出顺序为high-task (priority5, seq2) early-middle-task (priority2, seq3) middle-task (priority2, seq1)等等这里的输出好像跟预期不一致同优先级下 “early-middle-task(seq3)” 比 “middle-task(seq1)”先出队了。为什么因为我在TaskCompare里写的“seq小的在前”是a.seq b.seq按堆的语义这一规则实际会导致seq大的先出。对这就是容易踩坑的点在priority_queue里比较器语义与“队首优先级”之间的关系不是直观的等价关系你需要反向验证。为了达到“seq小的先出”我要反过来写a.seq b.seq但其实这里为了演示效果正确应该在同优先级时写return a.seq b.seq;吗我重新改一下因为上面输出有误。其实比较器里同优先级应该写成return a.seq b.seq;这样对堆来说seq“小”的被判定为优先级更高堆顶会是seq最小的。让我修正后的代码struct TaskCompare { bool operator()(const Task a, const Task b) const { if (a.priority ! b.priority) return a.priority b.priority; // 高优先级先出 return a.seq b.seq; // seq小的先出 } };用这个比较器堆顶应该先取到 priority5 的然后是 priority2 且 seq1 的再是 priority2 且 seq3 的。输出应该是high-task middle-task early-middle-task我上面演示输出写错了就是因为在优先级队列上搞反了先后。这是一个绝佳的反面教材——你以为你写对了实际却是反的。强烈建议读者以后写自定义比较器时先写一个小例程push三个元素验证top顺序。4.2 用lambda替代仿函数优先级队列中的现代写法如果你不太喜欢为一个比较器单独写一个类虽然我会说写类更清晰尤其是比较器想要复用或携带更多状态时C11之后也可以用lambda因为lambda本身就是一个可调用对象。不过需要引出一个陷阱priority_queue的类型声明里第三个模板参数是类型而lambda表达式的类型是独特的、匿名的。因此你不能直接把lambda当作类型传入必须先声明一个lambda变量然后使用decltype推导它的类型。#include iostream #include queue #include vector struct Task { int priority; int seq; std::string name; }; int main() { auto cmp [](const Task a, const Task b) { if (a.priority ! b.priority) return a.priority b.priority; return a.seq b.seq; }; std::priority_queueTask, std::vectorTask, decltype(cmp) queue(cmp); queue.push({2, 1, middle-task}); queue.push({5, 2, high-task}); queue.push({2, 3, early-middle-task}); while (!queue.empty()) { Task t queue.top(); std::cout t.name \n; queue.pop(); } }这里有一个必须注意的细节lambda默认是const的。当你实例化priority_queue时容器内部会持有这个cmp对象。在堆算法内部调用cmp(a,b)时你的lambda的operator()是const的。如果你在lambda里要修改捕获的变量比如一个计数器和配置默认不行需要加mutable。但是加了mutable的lambda它生成的operator()不再是const与priority_queue内部要求很可能冲突又会导致一堆冗长报错。所以在priority_queue里用的lambda我强烈建议不要有状态、不要mutable只做纯比较。你可能还会遇到一个常见错误decltype(cmp)类型在lambda变量的生命周期结束后依然可以实例化priority_queue吗没问题priority_queue内部会拷贝一份cmp对象而且构造函数显式接收cmp参数所以queue(cmp)是必要的。如果只写std::priority_queueTask, std::vectorTask, decltype(cmp) queue;它会尝试默认构造一个decltype(cmp)类型的对象但lambda没有默认构造函数于是编译就挂了。重要提醒用lambda做compare时priority_queue构造函数必须传入lambda实例。很多人只改了模板参数忘了传构造参数结果用一个“不存在的默认构造”报错非常经典。4.3 兼容性的默认构造比较器问题自定义仿函数类通常有默认构造函数所以你可以写std::priority_queueTask, std::vectorTask, TaskCompare queue; // OK默认构造TaskCompare但如果你的仿函数类构造函数需要参数就必须在priority_queue构造时传进去struct FlakyCmp { bool reverse; FlakyCmp(bool r) : reverse(r) {} bool operator()(int a, int b) const { return reverse ? a b : a b; } }; std::priority_queueint, std::vectorint, FlakyCmp pq(FlakyCmp(true)); // 传入 reversetrue则变成小顶堆这给我们的启发是优先级队列的比较器实际上可以是“有配置”的。你可以根据运行时状态创建不同行为比如系统负载高时要求紧急类型先出队。只要在构造队列时传入相应比较器对象即可。这是仿函数相比函数指针最优雅的地方之一因为函数指针没有“对象实例”概念挂载不了运行时配置。4.4 常用谓词工具std::less、std::greater、std::greater_equal等STL内置了许多函数对象比如std::lessT、std::greaterT、std::less_equalT、std::greater_equalT、std::equal_toT、std::not_equal_toT。它们都是模板类内部实现了operator()返回比较结果。在priority_queue里最常用的就这么几个。我把常见组合整理成速查表目标行为Compare模板参数说明大顶堆最大值在堆顶std::lessint默认情况int下就是大根堆小顶堆最小值在堆顶std::greaterint力扣题常客字符串字典序最大者std::lessstd::string返回字典序最大的字符串按绝对值最大自定义仿函数或lambda自行实现abs比较很多人在LeetCode上写“求数据流中的第K大”时会需要一个小顶堆std::priority_queueint, std::vectorint, std::greaterint minHeap;你会发现一旦背住了这个结论写起来确实特别快。但如果你能理解我们在3.2节说的“Compare语义决定堆底”你就不需要背随时可以推导出来。5. 追溯编译错误与常见问题实录5.1 错误一自定义类型没有operator如果我们直接写struct Task { int priority; }; std::priority_queueTask pq; // 错误 C2672 / C2676因为priority_queue默认使用std::lessTask要求Task支持operator。解决方式两种给Task重载operator。显式传入Compare并实例化模板参数。我在实际项目里的建议是当Task是一个业务实体且可能存在多种排序维度时不要轻易重载operator因为它等于把这个类型的“单一顺序”固化下来。后续如果产品要求按deadline排序或者按创建时间排序你就得改operator或加新判断可能影响其他模块。使用独立Compare谓词业务的排序逻辑就全部集中在调度模块中清晰可维护。5.2 错误二lambda作为模板参数但不传对象前面提到过auto cmp [](const Task a, const Task b) { ... }; std::priority_queueTask, std::vectorTask, decltype(cmp) queue; // 错误lambda类型没有默认构造函数正确写法必须带构造函数参数std::priority_queueTask, std::vectorTask, decltype(cmp) queue(cmp);5.3 错误三谓词参数类型不匹配如果谓词写成bool operator()(Task a, Task b)而priority_queue的操作中会传const Task没问题可以隐式拷贝。但在性能敏感场景建议直接写成const Task避免不必要的拷贝。此外模板类型推导时如果你给的Compare只接受Task*而队列元素是Task会无法匹配。5.4 错误四把sort的比较器直接搬到priority_queue很多人在已经写好了sort的比较器后想直接复用到priority_queue。比如auto cmp [](int a, int b) { return a b; }; std::sort(v.begin(), v.end(), cmp); // 升序 std::priority_queueint, std::vectorint, decltype(cmp) pq(cmp); // 结果是 大顶堆最大值在堆顶。这个问题往往不是错误而是“结果和预期不符”。你以为是升序用的时候发现取出来的却是最大值。根源就在sort与priority_queue对于Compare的语义约定是相反的sort的Compare返回true表示“a应在b前面”。priority_queue的Compare返回true表示“a的优先级低于b应沉在堆底”。所以在sort里的升序比较器a b放到priority_queue中恰恰构建出大顶堆。如果你希望实现“按升序pop”反而要写成a b。这个差异极容易让迁移代码的人错乱我就在一次改造日志系统时踩过把sort的lambda直接抄进priority_queue结果告警任务总是最高优级的先出导致低优级任务一直被饿死。排查了很久才意识到是这层语义差异。建议永远不要在priority_queue和sort之间复用同一个比较器除非你仔细推导过。最好统一加上注释写明“这个比较器服务于priority_queue较大的优先级更大”。5.5 错误五忘记pop()导致top()悬空或死循环这是一个逻辑错误。priority_queue的top()只返回引用不删除元素。如果你只调用top而不pop它永远是同一个元素如果你在while循环里不写pop()程序死循环。这个看似小儿科但在维护别人的烂代码时见得特别多。另外top()返回的是const引用如果想修改它你一般需要const_cast或者直接pop再push新元素。在优先队列里修改“已经在队列中的元素”是危险的因为堆性质建立在原有比较关系上。一旦修改了一个元素的值整个堆结构可能失效。正确做法是如果要更新某个任务的优先级先把它移出去或者标记失效再push一个新的。罪过做法就是直接改引用数据然后发现堆顶元素已经不符合“优先级最高”的语义了。5.6 错误六只想统计数量却误用priority_queue有些场景你只是想快速找到最大或最小的几个元素就用priority_queue。这没问题。但如果你的需求是“动态维护中位数”一个priority_queue就不够了需要一个大顶堆一个小顶堆配合。理解priority_queue之后这些延伸应用就水到渠成。比如我做过一个“在大量任务中获取最近截止时间的Top-K”的功能用小顶堆存当前最大的K个任务对截止时间而言是最小维护堆内元素不超过K。每次新任务来若堆满了且新任务的截止时间大于堆顶就替换。这个算法复杂度为O(N log K)远比排序后取前K高效。6. 从“会用”到“用好”我的体会与建议6.1 优先队列的性能特性priority_queue的操作复杂度非常清晰pushO(log N)popO(log N)topO(1)所以它以对数复杂度完成了“有序”的大部分工作代价是你无法遍历所有元素查找指定任务。它的底层vector在内存中是连续的但同时因为有堆结构并非完全有序。如果你需要随时随机访问中间元素priority_queue不是合适选择可以考虑std::multiset或平衡树容器。这在任务调度器设计时值得斟酌既要动态增加任务又要定期扫描所有任务那可能得换容器了。6.2 谓词的现代替代与性能权衡Lambda表达式写起来非常简洁。但如果你需要一个有名字、可复用、可状态化的比较器传统仿函数类更合适。特别在大型代码库中给比较器起个好名字比如TaskDeadlineAscending要比注释一行“这个lambda是干嘛的”更可靠。在C20之后你还可以用函数对象 operator等新特性但万变不离其宗核心还是可调用对象与operator()语义。有一段时间我沉迷于到处写lambda后来在review同事代码时发现许多人把几十行的lambda塞进模板参数里导致阅读困难。我的个人标准是超过10行的比较逻辑必须抽成类或具名函数只有极简比较一两行时才用lambda。这是代码可维护性的问题不是性能问题。6.3 用谓词正确表达业务规则当你把业务规则写进谓词时注意谓词的“纯净性”。所谓纯净性就是给定相同输入始终返回相同输出不修改对象状态不依赖外部可变全局状态。优先队列内维护的堆有效性依靠比较器的稳定性。如果比较器在运行过程中结果变化了比如依赖了某个外部计数器堆结构会破坏priority_queue就无法正确工作。我实际见过一个极端案例某模块的Compare里使用了当前系统时间导致同一对元素在不同时刻比较结果不一致堆完全乱套。表面上看Bug很难复现最后定位到谓词不纯净。从那以后我对每个仿函数的第一要求就是无副作用、结果确定、顺序一致。6.4 结构化的代码组织建议为了日常写priority_queue时少踩坑我一般按这样组织代码先定义任务/元素类型不内置比较关系除非只有一种天然顺序。定义一个独立的Compare仿函数或函数对象命名空间名字要能表达“哪个优先级更高”。在调用处写清楚是“大顶堆”还是“小顶堆”。我常用using别名来固化意图using TaskQueue std::priority_queueTask, std::vectorTask, TaskCompare;这样声明变量时就看懂了类型。这个习惯让调度代码非常清晰能反向规避“反向比较器”的坑。比如// 用法示例 TaskQueue highFirstQueue;只有看到TaskCompare定义才知道原来“high”的比较高优先级靠的是。为了更不容易出错我在命名时甚至会把比较规则写进名称里如TaskByPriorityDesc、TaskBySeqAsc。这让任何调用者一目了然。我开玩笑说在priority_queue的世界里“比较器”和“排序器”不能画等号只有根据“堆顶语义”去写才能一次到位。你能见到很多生产代码里函数名就叫HighPriorityFirst里面却是a.priority b.priority因为对priority_queue来说小值反而“沉底”。这一点不可不察。
阅读完成 · 觉得有帮助?