在定义泛型 fallback 时,我遇到了传统SFINAE(使用type_traits
和std::void_t
)与现代C++20 概念operator<<
之间的意外行为差异。目的很简单:创建一个泛型,仅当通过参数相关查找 (ADL)operator<<
未找到任何现有自定义定义时才启用。operator<<
使用特征( )的传统基于SFINAEis_std_streamable
的检测工作符合预期,其定义为:
template <class T, class = void>
struct is_std_streamable : std::false_type {};
template <class T>
struct is_std_streamable<T, std::void_t<decltype(std::declval<std::ostream&>() << std::declval<const T&>())>> : std::true_type {};
基于概念的检测(StdStreamable
)定义为:
template <class T>
concept StdStreamable = requires(const T t, std::ostream& os) {
{ os << t } -> std::same_as<std::ostream&>;
};
通用的后备operator<<
看起来像这样(requires
注释掉的子句):
template <StdPrintable T>
// requires(!StdStreamable<T>)
// requires(!is_std_streamable<T>::value)
std::enable_if_t<!is_std_streamable<T>::value, std::ostream&>
operator<<(std::ostream& os, T const& val) {
...
}
取消注释基于概念requires
的子句(requires(!StdStreamable<T>)
或)时requires(!is_std_streamable<T>::value)
,GCC和Clang都会产生以下循环约束错误:
error: satisfaction of constraint 'StdStreamable<T>' depends on itself
我理解,在定义新版本的子句std::declval<std::ostream&>() << std::declval<const T&>()
中使用表达式可能会被编译器解释为循环依赖。但为什么C++20 概念会触发此循环约束问题,而传统的SFINAE不会?这种行为是标准规定的,还是概念的已知限制,或者可能是编译器错误?requires
operator<<
完整、最小、可重现的示例和其他详细信息:
提前致谢。
这是 ODR 违规行为,你的程序格式不正确,你正在尝试执行
这是被禁止的,标准规定
StdStreamable<T>
或者is_std_streamable<T>
会根据程序中实例化的位置而赋予不同的含义。SFINAE 不需要诊断这个错误,因为替换失败不是一个错误,而且事实上它是一个不需要诊断的错误,所以他们就接受了这个错误。
编译器能够以检测这种形式的错误的方式实现概念。
您可以使用一个自由函数,根据它们中的哪个被定义来从
std::format
或object.print
或中进行选择ostream.operator<<
,但如果它不存在,您就无法使用模板元编程定义其中一个。歧义重载的问题来自于成员函数定义,
std::ostream::operator<<()
您的概念应该仅针对成员函数定义进行检查。您的通用自由模板函数“operator<<()”必须检查该概念,并且不得针对 STL 中的非通用模板自由函数进行任何专门化。
如果您的运算符重载对流使用了专门的类型,那么它总是更好的匹配。
考虑到这一点,你必须写下:
通用独立函数不需要受到其他独立函数的限制,因为它们已经通过选择“更好的匹配”进行了选择。
完整示例:https://godbolt.org/z/KKe46fsd1
但这是一个有点学术的解决方案,因为它对于现实世界的程序来说没有什么意义,因为程序中的“所有”其他类型都必须提供
print()
。在这种情况下,检查类型是否具有print()
函数要容易得多。没有“通用”函数,因为您必须了解所有可打印自定义类型的内部情况。