编译器约束验证顺序差异及相关C++代码合法性判定
关于C++概念与SFINAE的编译器行为疑问
问题代码与编译情况
以下代码:
#include <concepts> #include <string> #include <type_traits> template <std::signed_integral T> std::make_unsigned_t<T> to_unsigned(T t) { return t; } template <typename T> concept CanMakeUnsigned = requires(const T &t){to_unsigned(t);}; static_assert(!CanMakeUnsigned<std::string>);
在GCC 11及更高版本、搭配libc的Clang中可正常编译,但在MSVC、搭配libstdc的Clang、GCC 10及更早版本中,会在std::make_unsigned_t处触发硬错误。
请问哪个编译器的处理是正确的?该代码是否属于合法的C代码?看起来libc让std::make_unsigned_t支持SFINAE(不确定是否符合标准),但其余编译器的行为令人费解。
自定义复现代码
我尝试用自定义类型复现该问题,但所有编译器均能接受以下代码:
template <typename T> concept C = sizeof(T) == 42; template <typename T> struct A { T *x; }; template <C T> A<T> blah() {return {};} template <typename T> concept CanBlah = requires{blah<T>();}; static_assert(!CanBlah<int &>);
解答
标准规定与编译器行为分析
根据C++标准,std::make_unsigned_t<T>仅当T是整数类型(含bool、字符类型)或枚举类型时才合法,对其他类型实例化该模板会触发硬错误——即这种错误不属于SFINAE的范畴,编译器必须直接报错,而非忽略无效候选。
- MSVC、搭配libstdc++的Clang、GCC 10及更早版本的处理符合标准:它们严格遵循标准要求,对
std::make_unsigned_t<std::string>的非法实例化直接抛出硬错误。 - GCC 11+和搭配libc++的Clang的行为属于实现扩展:这些实现通过额外的SFINAE约束(比如
enable_if)让std::make_unsigned对非法类型友好,但这并非标准强制要求的行为。
自定义代码与原代码的本质区别
自定义代码中,blah<T>()受概念C约束,当T=int&时,sizeof(int&)不等于42,导致blah<int&>不满足约束。根据C++概念的规则,在requires表达式中,不满足约束的函数调用会被视为无效候选,但不会触发硬错误——这是标准明确规定的行为,因此所有编译器都能正确处理。而原代码的问题在于,std::make_unsigned_t的非法实例化是标准规定的硬错误场景,和概念约束不满足的情况完全不同。
结论
- 严格符合标准的行为是MSVC、搭配libstdc++的Clang、GCC 10及更早版本的处理方式。
- 原代码不属于合法的C++代码:检查
CanMakeUnsigned<std::string>时,会尝试实例化to_unsigned<std::string>,进而触发std::make_unsigned_t<std::string>的非法实例化,这是标准规定的硬错误场景。 - 部分编译器的兼容编译是扩展行为,并非标准要求。
内容的提问来源于stack exchange,提问作者HolyBlackCat
相关产品推荐
相关产品推荐

