递归noexcept()的含义解析:为何要嵌套noexcept表达式?
noexcept(noexcept(...))? 首先先把你提到的标准库swap定义贴出来,方便咱们对照看:
// 数组的swap模板 template <class T, size_t N> void swap(T (&a)[N], T (&b)[N]) noexcept(noexcept(swap(*a, *b))); // std::pair的swap成员函数 template <class T1, class T2> struct pair { // ... void swap(pair& p) noexcept(noexcept(swap(first, p.first)) && noexcept(swap(second, p.second))); // ... };
你疑惑的点非常关键——为啥不直接写noexcept(swap(*a, *b)),非要套一层noexcept()?其实核心是要分清noexcept的两种完全不同的用法:
1. 两种noexcept的区别
- 外层的
noexcept(...)是函数异常说明符:它用来告诉编译器“这个函数是否不会抛出异常”,它的参数必须是一个编译期布尔常量。 - 内层的
noexcept(表达式)是noexcept运算符:这是一个编译期运算符,它的作用不是标记函数,而是查询给定表达式是否具备noexcept属性,最终返回一个bool类型的编译期常量(true表示表达式不会抛异常,false则相反)。
2. 直接写noexcept(swap(*a, *b))为啥不行?
咱们想一下:swap(*a, *b)这个表达式的返回值是void(因为swap函数都是返回void的)。如果把它直接放到外层noexcept()里,就相当于写noexcept(void)——但异常说明符要求参数必须是bool常量,void类型根本不合法,编译器直接就会报错。
而套一层内层的noexcept运算符后,noexcept(swap(*a, *b))会把“调用swap(*a, *b)这个动作是否会抛异常”转换成一个bool常量,刚好符合外层异常说明符的要求。
3. 举个实际的例子
比如咱们有一个Widget类,它的swap是noexcept的:
class Widget { public: void swap(Widget& other) noexcept { /* 交换逻辑 */ } }; // 全局swap适配 void swap(Widget& a, Widget& b) noexcept { a.swap(b); }
这时候,noexcept(swap(Widget{}, Widget{}))这个运算符会返回true,所以数组swap的异常说明noexcept(noexcept(...))就等价于noexcept(true),也就是数组的swap会被标记为noexcept。如果Widget的swap不是noexcept的,那内层运算符返回false,数组swap也就不是noexcept的——这正好实现了“数组swap的noexcept属性和元素swap一致”的需求。
std::pair的情况也是同理:它的swap是否noexcept,取决于first和second的swap是否都noexcept,所以用两个noexcept运算符的结果做逻辑与,再传给外层的异常说明符。
总结一下
嵌套的noexcept(noexcept(...))其实是把两个功能组合起来:用内层的运算符查询表达式的异常属性,再用外层的说明符把这个属性赋予当前函数。如果不嵌套,要么会因为参数类型不合法编译失败,要么根本达不到“继承底层操作异常属性”的目的。
内容的提问来源于stack exchange,提问作者Birchlabs

