noexcept 说明符与 noexcept运算符各版本异同

学习内容

本节学习 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是无法管理的
相关推荐
代码中介商1 小时前
C语言预处理指令深度解析:从宏定义到条件编译
c语言·开发语言
hhb_6181 小时前
Groovy语法进阶与工程实践指南
开发语言·python
沐知全栈开发2 小时前
R CSV 文件处理指南
开发语言
极客BIM工作室2 小时前
OCCT开发实践:空间封闭曲线生成曲面的思考与总结
c++
秋92 小时前
OceanBase与GreatSQL在Java应用中的性能调优方法有哪些?
java·开发语言·oceanbase
GeLx2 小时前
从反爬角度:Playwright CDP 模式、Playwright 传统模式与 DrissionPage 的比较
python·程序人生·playwright·drissionpage·pyppeteer·浏览器自动化控制
澈2072 小时前
C++多态编程:从原理到实战
开发语言·c++
今天又在写代码2 小时前
并发问题解决
java·开发语言·数据库