在派生类中使用C++20太空船运算符遇跨类比较异常问题
问题根源:两种实现的核心差异与重载决议行为
1. 手动定义比较运算符的行为
你手动编写的operator<和operator>是全局非成员函数,参数为const ITimestampWrapper&。当比较任意派生类对象时:
- 派生类对象会被隐式切片为基类引用,直接调用这些函数。
- 函数内部仅比较基类的
_timestamp成员,完全符合你“只比较基类时间戳”的需求——无论派生类类型是否相同,行为都一致。
2. 默认太空船运算符的行为
当你在基类中使用= default定义operator<=>时:
- 基类会生成一个默认的三路比较运算符,仅比较基类的
_timestamp成员,编译器也会自动生成对应的operator>等运算符。 - 但你的派生类
testClass和testClass2没有显式声明比较运算符,编译器会自动为每个派生类生成默认的operator<=>。这个派生类的默认运算符会先比较基类部分,再比较派生类自己的成员(比如_x)——不过这只影响同类型派生对象的比较(你的同类型比较正常,是因为基类部分的比较已经决定了结果)。
关键问题出在跨类型派生对象的比较:
虽然基类的operator>理论上支持派生类的隐式转换,但在重载决议中,编译器可能无法正确选择这个候选。原因是:不同派生类之间没有直接的转换关系,编译器在处理跨类型比较时,可能误判候选的优先级,甚至在某些情况下触发对象地址的比较(而非成员变量的比较),导致结果不符合预期。
解决方案:确保跨类型比较只使用基类逻辑
要让默认太空船运算符的行为和手动定义的一致,你需要禁止派生类生成自己的默认比较运算符,并明确指定跨类型比较使用基类的逻辑:
方法1:将基类的operator<=>定义为成员函数
将基类的三路比较运算符改为成员函数,而非友元,这样派生类的隐式转换会更明确:
class ITimestampWrapper { public: ITimestampWrapper() noexcept : _timestamp(std::chrono::steady_clock::now()) { } explicit ITimestampWrapper(std::chrono::steady_clock::time_point timestamp) noexcept : _timestamp(timestamp) { } #ifdef USE_SPACESHIP // 成员函数形式的三路比较运算符 auto operator<=>(const ITimestampWrapper& rhs) const = default; #else friend bool operator<(const ITimestampWrapper& lhs, const ITimestampWrapper& rhs) { return lhs._timestamp < rhs._timestamp; } friend bool operator>(const ITimestampWrapper& lhs, const ITimestampWrapper& rhs) { return lhs._timestamp > rhs._timestamp; } #endif virtual ~ITimestampWrapper() = default; private: std::chrono::steady_clock::time_point _timestamp; };
方法2:显式禁用派生类的默认比较运算符
在派生类中删除自己的默认比较运算符,强制编译器使用基类的逻辑:
class testClass : public ITimestampWrapper { public: testClass() = default; explicit testClass(std::chrono::steady_clock::time_point test) : ITimestampWrapper(test) { } int _x = 0; // 删除派生类自己的默认比较运算符,强制使用基类逻辑 auto operator<=>(const testClass&) const = delete; }; class testClass2 : public ITimestampWrapper { public: testClass2() = default; explicit testClass2(std::chrono::steady_clock::time_point test) : ITimestampWrapper(test) { } int _x = 0; auto operator<=>(const testClass2&) const = delete; };
总结
手动定义的比较运算符通过明确的非成员函数签名,确保所有派生类对象的比较都切片到基类,行为一致。而默认生成的太空船运算符会导致派生类生成自己的比较运算符,在跨类型比较时可能触发重载决议的问题,导致不符合预期的行为。通过调整基类比较运算符的定义方式,或显式禁用派生类的默认比较运算符,可以解决这个问题。
内容的提问来源于stack exchange,提问作者HowP
相关产品推荐
相关产品推荐

