AppleClang下decltype推导ostream流插入表达式类型的异常结果探究
stream_t<T>推导差异的原因 这个现象的核心原因在于LLVM的libc++(AppleClang默认使用的标准库)为部分类型提供了针对右值std::ostream的operator<<重载,而GCC的libstdc++没有实现这些重载,导致两者在类型推导上出现差异。下面我们一步步拆解细节:
1. std::declval的返回值差异
首先明确两个declval调用的返回类型:
std::declval<std::ostream>():返回std::ostream&&(右值引用),因为declval<T>默认返回T&&。std::declval<std::ostream&>():根据C++的引用折叠规则,std::add_rvalue_reference_t<std::ostream&>等价于std::ostream&(左值引用),所以这个调用返回左值引用。
2. libc++中的右值operator<<重载
在libc++中,为了支持移动语义(比如允许std::move(std::cout) << "hello"这类写法),标准库为部分类型(如std::string、const char*、char等)提供了额外的全局operator<<重载,形式大致如下:
template <class CharT, class Traits> std::basic_ostream<CharT, Traits>&& operator<<(std::basic_ostream<CharT, Traits>&& os, const char* s) { os << s; return std::move(os); }
当你用右值std::ostream&&作为第一个参数调用operator<<时,会优先匹配这个右值重载,返回std::ostream&&——这就是你看到std::string、char等类型推导结果为右值引用的原因。
而对于int、float这类内置类型,它们的operator<<是std::ostream的成员函数,且这些成员函数没有添加右值限定符(即&或&&后缀)。根据C++规则,右值对象可以调用不带限定符的成员函数,此时函数返回的是std::ostream&(左值引用),所以内置类型的stream_t<T>推导结果符合预期。
3. libstdc++的处理方式
GCC的libstdc++没有实现上述针对右值ostream的operator<<重载,所有的operator<<都只接受左值引用的ostream作为第一个参数。因此,即使你传入std::ostream&&,编译器最终会匹配左值版本的重载,返回std::ostream&,所以GCC下所有类型的stream_t<T>推导结果都是左值引用。
4. 替换为std::declval<std::ostream&>后的统一结果
当你把第一个参数换成左值引用std::ostream&时,所有的operator<<调用都会匹配左值版本的重载(不管是成员函数还是全局左值重载),这些重载都返回std::ostream&,所以所有类型的stream_t<T>推导结果都统一为左值引用,符合你的预期。
总结一下:差异的根源在于不同标准库对右值ostream的operator<<支持策略不同,libc提供了右值重载,而libstdc没有,导致类型推导结果不同。
内容的提问来源于stack exchange,提问作者mkl

