libstdc++中std::compare_three_way指针转换及对比的实现疑问
问题
下面是libstdc++里std::compare_three_way的实现代码,想请教三个问题:
- 两处
reinterpret_cast<__UINTPTR_TYPE__>的设计依据是什么? - 为什么常量求值上下文里能直接比较
void*,但运行时必须转成__UINTPTR_TYPE__再比? - 既然
reinterpret_cast不属于常量表达式,为什么运行时非得用这个转换?
if constexpr (__detail::__3way_builtin_ptr_cmp<_Tp, _Up>) { auto __pt = static_cast<const volatile void*>(__t); auto __pu = static_cast<const volatile void*>(__u); if (__builtin_is_constant_evaluated()) return __pt <=> __pu; auto __it = reinterpret_cast<__UINTPTR_TYPE__>(__pt); auto __iu = reinterpret_cast<__UINTPTR_TYPE__>(__pu); return __it <=> __iu; } else return static_cast<_Tp&&>(__t) <=> static_cast<_Up&&>(__u);
解答
1. reinterpret_cast<__UINTPTR_TYPE__>的设计依据
C++标准明确规定:只有指向同一数组的元素、或者同一对象的子对象/成员的指针,直接用<=>或<这类运算符比较才是有定义的行为。如果是两个完全无关的指针(比如分别指向堆上两个独立对象),直接比较void*的结果是未定义的。
而__UINTPTR_TYPE__是编译器实现的、和指针宽度匹配的无符号整数类型(通常对应标准里的uintptr_t)。标准保证任何能转成void*的指针,都可以安全转成这个整数类型,转回去也能得到原指针。把指针转成整数后再比较,结果完全合法且稳定——不管指针指向什么,整数比较都能给出确定的全序关系,刚好满足std::compare_three_way对指针类型的要求:必须提供全序比较,而非标准限定的有限合法场景。
2. 常量求值与运行时的差异
在常量求值上下文(比如编译期计算常量表达式)里,编译器处理的都是编译期地址已知的对象,比如全局变量、静态变量。这些对象的地址在编译阶段就确定了,此时直接比较void*的行为是编译器可以明确处理的,不会出现未定义行为,所以直接用__pt <=> __pu没问题。
但运行时就不一样了:指针可能指向栈内存、动态分配的堆内存,这些地址编译期根本没法确定。如果这时候直接比较两个无关的void*,就触发了标准里的未定义行为——不同编译器可能给出不同结果,甚至程序崩溃。转成整数后比较就能避开这个坑,得到一致的结果。
3. 运行时必须用reinterpret_cast的原因
首先,reinterpret_cast不能出现在常量表达式里,但这里的转换是放在__builtin_is_constant_evaluated()为false的分支里——也就是说,只有在运行时才会执行这段代码,常量求值的时候根本不会走到这里,完全不违反常量表达式的规则。
其次,运行时必须用这个转换,核心原因就是避免直接比较无关指针的void*带来的未定义行为,同时通过整数比较提供std::compare_three_way要求的全序关系。
内容的提问来源于stack exchange,提问作者florestan

