C++ operator==:默认比较行为与常见陷阱解析

📅 发布时间:2026/9/10 0:33:34
C++ operator==:默认比较行为与常见陷阱解析
1. 默认比较的真相为什么看起来没错会变成线上事故先说一个我接手的真实 bug。项目里有个Config类负责承载业务运行时配置代码里用std::setConfig做去重用if (cfg lastCfg)判断配置是否发生变化。某次发版后线上日志显示配置在每次请求时都被判定为变了set 里攒了几千个内容几乎完全相同的对象。排查了两天才发现问题出在有人往类里加了一个毫秒级时间戳成员记录配置的生成时间而operator还是老样子只比较了业务字段。时间戳不同内容相同的配置就被判成了不相等。这种问题在 C 项目里太常见了。operator不是一个普通的成员函数它定义了一个类型在业务语义上的等价关系而这个关系会被std::find、std::set::find、unordered_map的查找、去重逻辑、状态对比反复复用。你写错一个operator影响的不是某一个if分支而是所有依赖比较语义的容器和算法的整体行为。所以在讨论怎么写之前先把默认行为到底比了什么讲清楚。1.1 内置类型、数组和类类型的三套规则C 里相等对不同类型有三套完全不同的规则这个必须先分清楚。第一套是内置类型的规则。int、double、char、指针这些类型比较的是它们存储的值本身。两个double比较比的是二进制浮点值两个指针比较比的是地址。这个行为没有什么可商量的就是存的值一样就相等。第二套是数组的规则。arr1 arr2里数组名会退化成指向首元素的指针所以这个表达式比较的是首元素地址而不是数组内容。这是从 C 时代遗留下来的坑无数新手在这里翻车。C 里想要按内容比较老老实实用std::array、std::vector或者std::string。第三套是类类型的规则。在 C17 及以前一个 class 如果没有声明operator你写a b直接编译报错编译器不会自动帮你生成。到了 C20情况变了编译器可以在满足条件时默认生成operator行为是逐成员调用各成员自己的operator。注意这个默认行为是逐成员的看起来省事实际上藏着不少坑下一节展开说。1.2 C20 默认生成的 operator 到底在比什么C20 的默认生成有个前提类里没有用户声明的operator并且所有成员都可以比较。生成的代码相当于把每个成员按声明顺序用比较一遍只要有一个不相等就返回 false。问题在于逐成员比较到底是深还是浅完全取决于成员的类型。如果成员是std::string、std::vector、std::map这些标准容器它们自己的operator是深比较会逐元素比较内容这个没问题。但如果成员是个裸指针默认比较的是指针地址不是指向的对象内容。举个例子struct User { const char* name; }; User a{alice}; User b{alice}; // 这里 a b 返回 false因为两个 name 指向的地址不同很多人写代码时脑子里想的是内容相等但默认实际做的是存储的值相等。指针的存储值是地址不是它指向的字符串。这个例子和 1.1 里数组退化的本质是一回事都是把位置当成了内容。另一个容易被忽略的是浮点成员。默认对浮点做的是精确值比较NaN永远不等于自己0.0和-0.0是相等的。如果你的业务语义需要的是容差比较默认行为显然不满足。C20 还有一个配套变化只要operator可用编译器就会自动把a ! b改写成!(a b)。所以在 C20 下你写一个就能同时获得!这是 C17 及以前做不到的。1.3 什么类型必须自己写 operator判断标准其实就一句话默认的结果是否完全等价于你业务上想要的相等。等价就用默认不等价就自己写。我整理了四类必须自己写的场景场景默认行为的问题需要做的含裸指针或智能指针且需要深比较默认比较地址解引用后比较内容处理空指针含浮点成员默认精确比较NaN 有问题按业务决定用位比较还是容差比较业务相等不等于全部成员相等默认比较所有成员只比较主键或关键字段继承体系下需要多态相等默认忽略派生信息用 typeid static_cast 或改用虚拟函数前两类场景在 3.1 和 3.2 会详细讲这里先记住结论能用默认的类通常是所有