C++17异常规范求值时机及相关代码合法性技术问询
咱们先从代码示例和编译器的行为差异说起,再结合C++17标准来解答你的问题:
代码版本与编译差异
首先看三个代码片段:
版本(1)
void q() {} class B { void f() noexcept(noexcept(q())) { q(); } decltype(&B::f) f2; };
版本(2)
void q() {} class B { void f() noexcept(true) { q(); } decltype(&B::f) f2; };
版本(3)
void q() {} class B { void f() noexcept { q(); } decltype(&B::f) f2; };
编译器表现:
- 所有GCC版本(包括最新主干版)编译这三个版本都没有错误或警告;
- 所有支持C++17的Clang版本会拒绝版本(1)和(2),只允许版本(3),报错信息如下:
<source>:4:16: error: exception specification is not available until end of class definition decltype(&B::f) f2; ^
这里要注意:C++标准明确规定noexcept(无参数形式)等价于noexcept(true)(出自[except.spec]章节),所以版本(2)和(3)理论上应该完全等价,但Clang的处理却不一致,这是我们要重点分析的点。
问题1:根据C++17标准,异常规范需要在哪个时机进行求值?
根据C++17标准的[class.mem]/6条款,在类定义的内部,非静态成员的完整声明(包括异常规范)要等到类定义结束(即}的位置)才会被完全确定——除非该成员的声明属于可以提前解析的特例。
而对于decltype(&B::f)这类表达式,关键在于:C++17开始,异常规范正式成为函数类型的一部分([except.spec]/12)。当你取成员函数指针时,必须获取该函数的完整类型,自然包括异常规范。
标准里的[except.spec]/11进一步说明:对于成员函数,异常规范的求值时机是当类定义完成时(如果是非模板函数),或者当模板被实例化时。这意味着在类定义内部(比如f2的位置),f的异常规范还没有完成求值,理论上此时无法获取包含异常规范的完整函数类型。
不过Clang对无参数的noexcept做了特殊处理:它认为这种形式的异常规范是隐式的true,可以在类内部直接确定,而显式的noexcept(true)或者依赖其他表达式的noexcept(noexcept(q()))则必须等到类定义结束才能完成求值——这也是为什么版本(3)能通过Clang编译,而版本(2)不行的原因,尽管标准说两者等价。
问题2:若上述部分代码无效,其背后的合理性是什么?
如果按照Clang的判定逻辑,版本(1)和(2)无效的核心原因是:
在类定义内部,当计算decltype(&B::f)时,B::f的完整函数类型(包含异常规范)还未确定。因为类成员是按顺序解析的,在f2的位置,类定义还未结束,f的异常规范可能依赖于类中后续声明的成员(虽然这个例子里没有,但标准要考虑通用情况),此时提前求值异常规范可能导致错误。
背后的设计合理性在于:类的成员声明是一个顺序依赖的过程,标准需要确保类内部的名称查找和类型计算不会因为未完成的声明而出错。如果允许在类内部提前获取包含异常规范的成员函数指针,那么当异常规范依赖后续成员时,就会出现无法解析的情况——所以标准规定,成员函数的完整类型要到类定义结束时才完全确定,此时才能安全地获取其函数指针。
GCC允许这三个版本编译,属于编译器实现上的宽松处理,它可能提前求值了异常规范,但这并不完全符合标准的严格要求。
补充背景摘要
在C++14及更早版本中,异常规范不属于函数类型的一部分,所以下面这段代码是有效的(Clang 3.9.1及之前版本可以正常编译):
template <typename F> struct result_type; template<typename R, typename C, typename... Args> struct result_type<R(C::*)(Args...)> { using type = R; }; // there may be other specializations ... class B { int f() noexcept(false) { return 3; } typename result_type<decltype(&B::f)>::type a; };
但C17改变了这一点,将异常规范纳入函数类型,导致decltype(&B::f)的类型包含了异常规范,而类内部无法提前获取完整的函数类型,因此这段代码在C17中无法编译。
内容的提问来源于stack exchange,提问作者overseas

