C++20默认比较运算符与空基类的兼容问题及解决方案问询
C++20引入的默认三路比较运算符(operator<=>)极大简化了类的比较逻辑实现,但当类继承自空基类时,这个特性的实用性会大幅下降——核心矛盾在于空基类本身不需要比较逻辑,却会干扰派生类的默认比较生成。
根据C++标准规定:
默认
operator<=>通过依次比较T的基类(从左到右深度优先),然后是非静态成员(按声明顺序)子对象来执行字典序比较,递归展开数组成员(按下标递增顺序),当发现非相等结果时提前终止。
现有问题分析
问题1:空基类无比较运算符时,派生类默认比较被删除
如果空基类未定义operator<=>,派生类的默认operator<=>会被编译器删除(Clang会给出明确警告),导致无法使用默认比较特性:struct base {}; struct SComparable: base { int m_n; auto operator<=> (const SComparable&) const& = default; // 编译器会删除该运算符,Clang发出警告 }; struct SNotComparable: base { int m_n; };问题2:给空基类加默认比较,会意外赋予所有派生类比较能力
若为了让目标派生类能用默认比较,给空基类添加默认operator<=>,则所有继承该空基类的派生类都会默认支持比较(始终返回std::strong_ordering::equal),包括那些不需要比较能力的类:struct base { auto operator<=> (const base&) const& = default; }; struct SComparable: base { int m_n; auto operator<=> (const SComparable&) const& = default; }; struct SNotComparable: base { // 意外获得了比较能力! int m_n; };问题3:手动删除派生类比较的弊端
部分方案建议给空基类加默认比较,再在不需要比较的派生类中显式删除operator<=>,但这种方式风险很高:- 常用空基类的所有派生类都会默认支持比较,需要逐一排查并删除不需要的,维护成本高;
- 若遗漏某个类,后续想为其添加自定义比较逻辑时,容易忽略默认比较的存在,导致错误的比较结果(始终返回相等),而非编译错误,违背了默认比较简化开发的初衷:
struct base { auto operator<=> (const base&) const& = default; }; struct SComparable: base { int m_n; auto operator<=> (const SComparable&) const& = default; }; struct SNotComparable1: base { int m_n; auto operator<=> (const SNotComparable1&) const& = delete; }; struct SNotComparableN: base { int m_n; // 遗漏删除比较运算符!后续若想添加自定义比较,容易因默认逻辑导致错误结果 };
推荐解决方案
针对上述问题,有两种安全且易维护的方案:
方案1:CRTP模板空基类,按需开放比较能力
通过CRTP(奇异递归模板模式)让空基类仅对需要比较的派生类开放operator<=>,其他派生类则无法继承比较能力:
#include <compare> #include <type_traits> // 模板空基类,仅当指定Derived时才提供比较运算符 template<typename Derived = void> struct base { // 仅当Derived不是void时,生成公开的默认比较运算符 friend auto operator<=>(const base&, const base&) requires (!std::is_same_v<Derived, void>) = default; }; // 不需要比较的派生类:继承base<void>,无公开比较运算符 struct SNotComparable : base<void> { int m_n; }; // 需要比较的派生类:继承base<当前类>,获得基类的比较能力 struct SComparable : base<SComparable> { int m_n; // 可以安全使用默认比较运算符,基类比较始终返回相等,不影响结果 auto operator<=> (const SComparable&) const& = default; };
这种方案的优势:
- 不需要比较的派生类默认无法生成比较运算符,避免意外;
- 需要比较的派生类只需修改继承的模板参数,即可直接使用默认比较;
- 空基类的比较逻辑仅对目标派生类可见,不会污染其他继承场景。
方案2:派生类手动实现比较,忽略空基类
如果空基类无法修改(比如来自第三方库),可以在需要比较的派生类中手动实现operator<=>,仅比较自身成员,完全忽略空基类的存在:
#include <compare> #include <tuple> struct base {}; struct SComparable : base { int m_n; // 手动比较成员,跳过空基类 auto operator<=>(const SComparable& other) const& { return std::tie(m_n) <=> std::tie(other.m_n); } // 复用默认的==运算符 bool operator==(const SComparable& other) const& = default; };
这种方案的优势:
- 无需修改空基类,适配第三方库场景;
- 逻辑清晰,明确跳过空基类的比较,避免潜在问题;
- 配合
std::tie可以快速实现多成员的字典序比较,接近默认比较的便捷性。
内容的提问来源于stack exchange,提问作者wanghan02

