根据 C++ 标准,具有不完整类型的 `sizeof(T)` 是否是有效的替换失败?

Is `sizeof(T)` with an incomplete type a valid substitution-failure as per the C++ Standard?

我在 Whosebug 和其他地方看到过几次 decltype(sizeof(T)) 可以与 std::void_t 一起使用到 SFINAE,无论 T 是否完整。 Raymond Chen 甚至在 Microsoft 的博客 Detecting in C++ whether a type is defined 中记录了这个过程,并明确指出:

I’m not sure if this is technically legal, but all the compilers I tried seemed to be okay with it.

根据 C++ 标准,此行为是否可靠且定义明确?

我能在标准中找到的唯一指示来自 [expr.sizeof]/1,其中指出:

... The sizeof operator shall not be applied to an expression that has function or incomplete type, to the parenthesized name of such types, or to a glvalue that designates a bit-field ...

然而,我不清楚“不得应用” 措辞是否意味着根据 [temp],或者这是否格式错误。

ℹ️ 注意:这个问题不针对标准的任何特定版本,但是如果这个标准在任何时候发生了变化,比较一下会很有趣.

“不得应用”意味着它通常是错误格式的。在 SFINAE 上下文中,如果某些内容通常由于导致“无效类型或表达式”而格式错误,只要它在“直接上下文”中(C++20 [temp.deduct]/8) 并且未以其他方式排除在 SFINAE 之外(例如 参见 p9 关于 lambda 表达式)。

在这种情况下,“无效”和“格式错误”之间没有区别。 p8 明确指出:“无效的类型或表达式是一种格式错误的类型或表达式,如果使用替换参数编写,则需要进行诊断。”这种措辞自 C++11 以来一直存在。但是,在 C++03 中,无效的 表达式 不是替换失败。这是 C++11 中添加的著名的“表达式 SFINAE”功能,编译器实现者充分相信他们能够实现它。

标准中没有规则说 sizeof 表达式是 SFINAE 规则的例外,因此只要在直接上下文中出现无效的 sizeof 表达式,SFINAE 就适用。

标准中仍未明确定义“直接上下文”。 GCC 开发人员 Jonathan Wakely 的 answer 解释了其意图。最终,有人可能会在标准中正式定义它。

然而,在类型不完整的情况下,问题是这种技术非常危险。首先,如果在同一类型的same翻译单元中进行了两次完整性检查,则只进行一次实例化;这意味着第二次检查时,检查结果仍为假,因为 is_type_complete_v<T> 将仅引用先前的实例化。 Chen 的 post 似乎完全错了:GCC、Clang 和 MSVC 都以相同的方式运行。参见 godbolt。旧版本的 MSVC 上的行为可能有所不同。

其次,如果存在跨翻译单元差异:即is_type_complete_v<T>在一个翻译单元实例化为假,在另一个翻译单元实例化为真,程序为格式错误的 NDR。见 C++20 [temp.point]/7.

因此,通常不进行完整性检查;相反,库实现者要么说你被允许将不完整的类型传递给他们的模板,它们将正常工作,要么你必须传递一个完整的类型,但如果你违反了这个要求,行为是未定义的,因为它不能在编译时可靠地检查时间。

围绕模板实例化规则的一种创造性方法是使用带有 __COUNTER__ 的宏来确保每次使用类型特征时都有一个新的实例化, 你必须定义带有内部链接的 is_type_complete_v 模板,以避免跨 TU 差异的问题。我从 this answer 那里得到了这个技巧。不幸的是,__COUNTER__ 不在标准 C++ 中,但该技术应该适用于支持它的编译器。

(我研究了 C++20 source_location 特性是否可以替代该技术中的非标准 __COUNTER__。我认为它不能,因为 IS_COMPLETE 可能从相同的行和列中引用,但在两个不同的模板实例中,它们以某种方式都决定检查相同的类型,其中一个不完整而另一个完整。)