Clang“exception specification未到类定义末尾不可用”Bug的规避方案
问题重现
首先来看你遇到的核心问题:下面这段代码在所有GCC版本(4.7.0及以上)都能正常编译,但在Clang 5.0.1等旧版本中会报错:
template <typename> struct Check { constexpr static bool value = true; }; struct A { A() noexcept(Check<int>::value) = default; };
编译行为差异
- GCC编译(正常):
$ g++-4.9.4 test.cpp -c -o test -std=c++11 -Wall -Wextra $ g++-5.4.0 test.cpp -c -o test -std=c++11 -Wall -Wextra # 更高版本GCC同样正常
- Clang编译(报错):
$ clang++ test.cpp -c -o test -std=c++11 -Wall -Wextra test.cpp:5:5: error: exception specification is not available until end of class definition A() noexcept(Check<int>::value) = default; ^ test.cpp:5:18: note: in instantiation of template class 'Check<int>' requested here A() noexcept(Check<int>::value) = default; ^
Bug追踪状态
你已经定位到这个是Clang的PR23383,这个问题和C++标准缺陷报告DR1330以及Clang的PR30860相关,本质是Clang对noexcept()表达式的解析时机处理有误——旧版本Clang会在类定义完成前尝试解析noexcept中的模板实例化,而标准允许延迟到类定义完成后处理。
好消息是Clang的主干版本已经修复了这个Bug,但旧版本(比如5.0.1)仍然存在这个问题。
基础场景的规避方案
针对基础场景,你提到的方案是可行的:把noexcept()中的表达式提取为类内的static constexpr成员,强制编译器在类内提前解析这个值,绕开Clang的解析时机问题:
template <typename> struct Check { constexpr static bool value = true; }; struct A { A() noexcept(workaround) = default; private: // 规避Clang PR23383:将noexcept表达式移到类内static constexpr成员 static constexpr bool workaround = Check<int>::value; };
这个方案的核心是让Check<int>::value的实例化发生在类内成员定义阶段,而不是构造函数的异常规格解析阶段,避免Clang的时机错误。
嵌套/继承复杂场景的问题分析
但你发现这个方案在嵌套类+继承的场景下,即使Clang主干版本也会报错:
#include <type_traits> struct Base { virtual ~Base() noexcept; }; struct OuterClass { class InnerDerived: public Base { private: static constexpr auto const workaround = std::is_nothrow_default_constructible<Base>::value; public: InnerDerived() noexcept(workaround); }; class InnerDerived2: public InnerDerived { private: static constexpr auto const workaround2 = std::is_nothrow_default_constructible<InnerDerived>::value; public: InnerDerived2() noexcept(workaround2); }; };
报错原因
这个问题不是PR23383的遗留问题,而是C++标准中关于类型特性检查和函数定义时机的规则导致的:
InnerDerived的构造函数只做了声明,没有在类内完成定义(= default放在类外的话,此时编译器还不知道它的具体异常规格)。std::is_nothrow_default_constructible<T>需要确定T的默认构造函数是否是noexcept的,但当InnerDerived2中定义workaround2时,InnerDerived的构造函数还没有完成定义,编译器无法确定它的异常规格,因此报错。
针对复杂场景的解决方案
有两种可行的修复方式:
方案1:将基类的默认构造函数定义放在类内
让InnerDerived的构造函数在类内完成默认定义,这样编译器就能在InnerDerived2中确定它的异常规格:
#include <type_traits> struct Base { virtual ~Base() noexcept; }; struct OuterClass { class InnerDerived: public Base { public: // 直接在类内完成默认构造函数的定义 InnerDerived() noexcept(std::is_nothrow_default_constructible<Base>::value) = default; }; class InnerDerived2: public InnerDerived { public: InnerDerived2() noexcept(std::is_nothrow_default_constructible<InnerDerived>::value) = default; }; };
方案2:用模板延迟类型特性检查
通过模板函数延迟std::is_nothrow_default_constructible的实例化时机,直到构造函数被使用时再解析:
#include <type_traits> struct Base { virtual ~Base() noexcept; }; struct OuterClass { class InnerDerived: public Base { public: InnerDerived() noexcept(std::is_nothrow_default_constructible<Base>::value); }; // 用模板函数延迟类型特性检查 template <typename T> constexpr bool is_nothrow_default() { return std::is_nothrow_default_constructible<T>::value; } class InnerDerived2: public InnerDerived { public: InnerDerived2() noexcept(is_nothrow_default<InnerDerived>()) = default; }; }; // 在类外完成InnerDerived构造函数的定义 OuterClass::InnerDerived::InnerDerived() noexcept(std::is_nothrow_default_constructible<Base>::value) = default;
这样就能绕开编译器在类定义阶段无法确定基类构造函数异常规格的问题。
内容的提问来源于stack exchange,提问作者jotik

