学习内容
本节学习 noexcept说明符与noexcept运算符在各版本的不同,后续请关注 学习C++11/14/17/20/23关键词版本更替 ,将持续更新~~
noexcept说明符与noexcept运算符区别
noexcept说明符: 用于声明函数是否抛出异常
noexcept运算符: 是编译期查询,返回bool
noexcept说明符
noexcept有3种写法,其中throw()在C++17废弃、C++20移除,只保留前两种
noexcept 等价于noexcept(true) ,声明函数绝不抛出异常
noexcept(expression) , expression为true则不抛出异常,false则可能抛异常(表达式需是编译期常量)
throw() ,旧版写法,等价于noexcept(true),C++17废弃、C++20移除
- 基础语法
cpp
void func1() noexcept { cout << "func1 不抛出异常" << endl; } //如果实际抛出异常,C++会直接调用terminate终止程序
constexpr bool flag = false;
void func2() noexcept(flag) { throw 1; }; //合法,因为noexcept(flag) = false //函数可能抛出异常
void func3() throw() { cout << "func3 旧版不抛异常声明" << endl; }
int main() {
func1();
try{
func2();
}catch(...) { cout << "catch func2 error" << endl; }
}
//lambda中noexcept的用法
auto func = []() noexcept{
// do something ,绝不抛出异常
};
// error
typedef int(*func)() noexcept; //不允许在typedef中使用
void f(void(*)() noexcept);
void g();
f(&g); // C++11不严格匹配,编译通过 ,C++17类型匹配错误
C++17之前规则, noexcept说明符不是函数类型的一部分,只能出现在以下位置
1、lambda表达式的声明符中
2、顶层函数声明符中,当声明函数、变量、非静态数据成员(类型为函数、函数指针、函数引用或成员函数指针时)
3、上述声明的参数或返回类型中
4、不可出现在typedef或类型别名声明中
C++17起,noexcept说明符是函数类型的一部分,或出现在任何函数声明符中。包括typedef等,类型检查更加严格
-
在C++11中,noexcept只是说明,不算类型,所以
- void() 与 void() noexcept 是同一个类型
- typedef void (*func)() noexcept; // error
- 函数指针作为参数,不严格匹配noexcept
-
在C++17中,noexcept是函数类型的一部分,所以
- void()与void() noexcept不是同一个类型,不能乱赋值
- typedef void (*func)() noexcept; // ok
- using fp = void(*)() noexcept; //ok
- 函数指针作为参数严格匹配
属于可能抛出异常的函数
- 声明了非空动态异常说明的函数throw(),C++17前有效
void func() throw(int); - 使用noexcept说明符表达式求值为false的函数
void func() noexcept(false); - 没有声明noexcept的函数(但以下特殊成员函数除外)
- 析构函数(它调用的基类/成员析构函数属于可能抛出异常的)
- 隐式声明或默认生成的默认/拷贝/移动构造函数(除非它们调用的基类/成员函数、初始化子表达式、默认成员初始化器都不抛出异常)
- 隐式声明或默认生成的拷贝/移动赋值运算符(除非它们调用的所有赋值运算符都不抛出异常)
- 默认生成的比较运算符(C++20起,除非它们调用的所有比较运算符都不抛出异常)
不抛出异常的函数: 除了以上的可能抛出异常的函数,其它都是不抛出异常的函数,包括
- noexcept / noexcept(true) 声明的函数
- 满足条件的析构函数、默认生成的特殊成员函数
- 解分配函数(如operator delete,默认不抛出异常)
重载与异常说明
仅异常说明不同的函数不能被重载
cpp
void func() noexcept;
void func(); // error,异常说明不同,无法重载 ,前者为不抛出异常,后者为可能抛出异常
void g() noexcept(false);
void g(); // ok , 两者都属于可能抛出异常函数,异常说明不冲突
函数指针的转换规则
指向不抛出异常函数的指针,可能被赋值给/隐式转换为指向可能抛出异常函数的指针(C++17前为直接赋值,C++17起隐式转换)
cpp
void func1(); // 可能抛出异常
void func2() noexcept; // 不会抛出异常
void (*pFunc2)() = &func2; // ok, 不抛出异常可转换为可能抛出异常
void (*pFunc1)() noexcept = &func1; // error , 可能抛出异常转为不抛出异常,不允许
虚函数与noexcept的重写规则
如果基类的虚函数是不抛出异常的noexcept(true),那么派生类中所有重写它的函数,都必须是不抛异常的,除非该重写函数被定义为=delete
cpp
struct exam{
virtual void f() noexcept;
virtual void g();
virtual void h() noexcept = delete; };
struct A : exam {
void f(); // error , f函数必须是noexcept,与基类的被重写虚函数相同
void g() noexcept; // ok , 比基类更加严格,是可以的
void h() = delete; //ok , 基类的h函数也=delete,符合规则 };
派生类重写的virtual函数,异常说明不能比基类更加宽松
当不抛异常的函数调用可能抛出异常的函数
结论是不抛异常的函数是可能调用可能抛出异常的函数,但是如果在这个过程中抛出了异常,并且异常处理的查找栈帧到达了noexcept函数的最外层时,程序会直接调用std::terminate终止运行
cpp
void func() ; //可能抛出异常
void g() noexcept { //声明为不抛出异常
func(); // 语法合法,如果f抛出异常,会触发std::terminate
throw 42; // 语法合法,运行会触发std::terminate
}
int main()
{
try {
g();
}catch(...){ // 永远不去执行,因为g抛出异常会直接std::terminate终止程序
std::cout << " catch exception " << std::endl;
}
}
函数模板特化的异常说明:延迟实例化
编译器在某个时刻需要知道该函数的类型或可行性,因此必须把函数模板的异常说明(noexcept表达式)用具体模板参数实例化并检查其合法性。只有在这种情况下,延迟实例化会被触发
常见的会触发实例化的场景
- 重载决议中被选中:为了判断哪一个重载可行或更好,编译器可能需要比较或形成候选函数的签名,从而实例化依赖的表达式
- 函数被实际调用或取地址时:显式需要函数的实际类型或实现,必须实例化
- 函数会被调用或取地址,但出来在未求值的操作数中,虽然不执行函数,但为了确定表达式的类型/类型属性,编译器需要知道函数的签名(包括依赖的noexcept),因此也需要实例化
cpp
template<class T>
T f() noexcept(sizeof(T) < 4);
int main() {
// 虽然没有调用 f<void>,但 decltype 需要知道它的 noexcept 说明
decltype(f<void>()) *p;
// 编译错误:sizeof(void) 是非法的,实例化 noexcept 说明时出错
}
- 在decltype(f)中,虽然没有调用f(),但为了决定decltype的结果类型,编译器必须知道f()的类型/签名
- 该签名包括了一个依赖模板参数的noexcept表达式noexcept(sizeof(T)<4) 为判断签名是否合法,编译器用T=void实例化noexcept表达式
- sizeof(void) 在C++中是非法的,因此会产生错误
*注: noexcept说明符不是编译期的强制检查,它只是程序员向编译器提供的函数是否会抛异常的信息。编译器会利用这些信息做优化,支持noexcept运算符,在编译期检查表达式是否声明为不抛异常
noexcept运算符
C++11引入的编译期运算符,作用是:
- 检查某个表达式是否被声明为不会抛出任何异常
- 返回一个编译期的bool常量 ,true表示该表达式被声明为不抛出异常,false为可能抛出异常
语法形式为: noexcept(expression) ,expression是未求值操作数,只是检查它的异常声明
返回值是bool类型的纯右值
如果expression是一个类类型或数组类型的纯右值,noexcept运算符会触发临时物化,代表:
- 它会隐式调用该类型的析构函数
- 要求析构函数必须是非删除、可访问的,否则编译报错
cpp
void funcNoThrow() noexcept{} //不抛出异常
void funcThrow() {} //可能抛出异常
cout << noexcept(funcNoThrow()) << endl;
cout << noexcept(funcThrow()) << endl;
- 注: noexcept(expr) == true ,不代表运行时一定不抛出异常
- noexcept(expr) 只检查声明,不检查运行时行为,如果表达式因为未定义行为抛出异常,noexcept是无法管理的