CPP异常处理之noexcept函数类型
Exception specifications and noexcept in CPP
Looking at a typical function declaration, it is not possible to determine whether a function might throw an exception or not:
int doSomething(); // can this function throw an exception or not?
In the above example, can the doSomething
function throw an exception? It’s not clear. But the answer is important in some contexts. If doSomething
could throw an exception, then it’s not safe to call this function from a destructor, or any other place where a thrown exception is undesirable.
While comments may help enumerate whether a function throws exceptions or not (and if so, what kind of exceptions), documentation can grow stale and there is no compiler enforcement for comments.
Exception specifications are a language mechanism that was originally designed to document what kind of exceptions a function might throw as part of a function specification.
The noexcept specifier
In C++, all functions are classified as either non-throwing or potentially throwing. A non-throwing function is one that promises not to throw exceptions that are visible to the caller. A potentially throwing function may throw exceptions that are visible to the caller.
在抛出异常处理时, C++中只有两种函数类型, 一种是保证不会抛出异常, 另一种是有可能抛出异常
To define a function as non-throwing, we can use the noexcept specifier. To do so, we use the noexcept keyword in the function declaration, placed to the right of the function parameter list:
void doSomething() noexcept; // this function is specified as non-throwing
Note that noexcept doesn’t actually prevent the function from throwing exceptions or calling other functions that are potentially throwing. This is allowed so long as the noexcept function catches and handles those exceptions internally, and those exceptions do not exit the noexcept function.
noexcept 函数并不代表在函数中不允许抛出异常, 只要这个函数能够在内部捕获并处理这个异常, 并且不将这个异常返回到外部调用函数, 就是一个noexcept函数.
If an unhandled exception would exit a noexcept function, std::terminate
will be called (even if there is an exception handler that would otherwise handle such an exception somewhere up the stack). And if std::terminate
is called from inside a noexcept function, stack unwinding may or may not occur (depending on implementation and optimizations), which means your objects may or may not be destructed properly prior to termination.
如果一个定义为 noexcept 的函数因为一个未处理的exception而退出, 那么就会调用
std::terminate
来结束程序, 即使更外层有捕获这个异常的函数.
stack unwinding是指在抛出异常的时候会根据函数调用栈递归的向外处理这个异常, 直到遇到找到匹配的 catch 来处理这个异常, 处理完这个异常之后, 函数会调用析构函数释放申请的内存, 防止内存泄漏
Much like functions that differ only in their return values can not be overloaded, functions differing only in their exception specification can not be overloaded.
因为 noexcept 标志并不是函数签名的一部分, 所以不允许只有 noexcept 不同的函数重载, 这是因为编译器无法区分重载函数与原函数.
Key insight
The promise that a noexcept function makes to not throw exceptions that are visible to the caller is a contractual promise, not a promise enforced by the compiler. So while calling a noexcept function should be safe, any exception handling bugs in the noexcept function that cause the contract to be broken will result in termination of the program! This shouldn’t happen, but neither should bugs.
For this reason, it’s best that noexcept functions don’t mess with exceptions at all, or call potentially throwing functions that could raise an exception. A noexcept function can’t have an exception handling bug if no exceptions can possibly be raised in the first place!
简而言之就是使用一个noexcept函数的时候一定要小心, 不要再内部处理混乱的 exceptions, 防止出错
Illustrating the behavior of noexcept functions and exceptions
下面这个例子很好的解释了 noexcept 函数正常以及异常处理的实例:
// h/t to reader yellowEmu for the first draft of this program
#include <iostream>
class Doomed {
public:
~Doomed() {
std::cout << "Doomed destructed\n";
}
};
// 抛出异常的函数
void thrower() {
std::cout << "Throwing exception\n";
throw 1;
}
void pt() {
std::cout << "pt (potentally throwing) called\n";
//This object will be destroyed during stack unwinding (if it occurs)
Doomed doomed{};
thrower();
std::cout << "This never prints\n";
}
// nt 是一个 noexcept, 需要在内部处理完异常
void nt() noexcept {
std::cout << "nt (noexcept) called\n";
//This object will be destroyed during stack unwinding (if it occurs)
Doomed doomed{};
thrower();
std::cout << "this never prints\n";
}
// noexcept函数内部有处理异常的功能, 但是是条件处理
void tester(int c) noexcept {
std::cout << "tester (noexcept) case " << c << " called\n";
try {
(c == 1) ? pt() : nt();
}
catch (...) {
std::cout << "tester caught exception\n";
}
}
int main() {
std::cout << std::unitbuf; // flush buffer after each insertion
std::cout << std::boolalpha; // print boolean as true/false
tester(1);
std::cout << "Test successful\n\n";
tester(2);
std::cout << "Test successful\n";
return 0;
}
这个程序的执行结果应该如下所示"
tester (noexcept) case 1 called
pt (potentially throwing) called
Throwing exception
Doomed destructed
tester caught exception
Test successful
tester (noexcept) case 2 called
nt (noexcept) called
throwing exception
terminate called after throwing an instance of 'int'
我们来解释一下这个函数的输出:
Let’s explore what’s happening here in more detail. Note that tester
is a noexcept function, and thus promises not to expose any exception to the caller (main).
The first case illustrates that noexcept functions can call potentially throwing functions and even handle any exceptions those functions throw. First, tester(1) is called, which calls potentially throwing function pt , which calls thrower, which throws an exception. The first handler for this exception is in tester, so the exception unwinds the stack (destroying local variable doomed in the process), and the exception is caught and handled within tester. Because tester does not expose this exception to the caller (main), there is no violation of noexcept here, and control returns to main.
这个例子在
tester(1)
函数调用的时候, 内层函数pt()
抛出了一个未处理的异常, 但是在tester
函数中, 有对这个异常的处理, 即使tester()
函数是一个 noexcept函数, 但是tester()
在内部处理了这个异常, 所以不会报错.
The second case illustrates what happens when a noexcept function tries to pass an exception back to its caller. First, tester(2) is called, which calls non-throwing function nt
, which calls thrower, which throws an exception. The first handler for this exception is in tester. However, nt is noexcept, and to get to the handler in tester, the exception would have to propagate to the caller of nt. That is a violation of the noexcept of nt, and so std::terminate
is called, and our program is aborted immediately. On the author’s machine, the stack was not unwound (as illustrated by doomed not being destroyed).
在这个例子中, 问题不在
tester()
函数, 而是在nt()
函数, 这是一个noexcept函数, 但是抛出了一个未处理的异常, 需要注意的是,std::terminate
是在这个 noexcept 函数退出后调用, 也就是在caller
中调用, 在nt()
函数结束之后调用.
The noexcept specifier with a Boolean parameter
The noexcept specifier has an optional Boolean parameter. noexcept(true) is equivalent to noexcept, meaning the function is non-throwing. noexcept(false) means the function is potentially throwing. These parameters are typically only used in template functions, so that a template function can be dynamically created as non-throwing or potentially throwing based on some parameterized value.
Which functions are non-throwing and potentially-throwing
Functions that are implicitly non-throwing:
Destructors
构造函数是隐式的 noexcept 函数, 不允许抛出未处理的异常
Functions that are non-throwing by default for implicitly-declared or defaulted functions:
- Constructors: default, copy, move
- Assignments: copy, move
- Comparison operators (as of C++20)
However, if any of these functions call (explicitly or implicitly) another function which is potentially throwing, then the listed function will be treated as potentially throwing as well. For example, if a class has a data member with a potentially throwing constructor, then the class’s constructors will be treated as potentially throwing as well. As another example, if a copy assignment operator calls a potentially throwing assignment operator, then the copy assignment will be potentially throwing as well.
上面这三个函数默认是 noexcept 的, 但是可以继承调用函数的 potentially throwing 属性, 例如内部函数是 potentially throwing, 那么上述函数就会变成 potentially throwing.
Functions that are potentially throwing (if not implicitly-declared or defaulted):
Normal functions
User-defined constructors
User-defined operators
上面是正常的potentially throwing函数.
Exception safety guarantees
An exception safety guarantee is a contractual guideline about how functions or classes will behave in the event an exception occurs. There are four levels of exception safety guarantees:
- No guarantee -- There are no guarantees about what will happen if an exception is thrown (e.g. a class may be left in an unusable state)
- Basic guarantee -- If an exception is thrown, no memory will be leaked and the object is still usable, but the program may be left in a modified state.
- Strong guarantee -- If an exception is thrown, no memory will be leaked and the program state will not be changed. This means the function must either completely succeed or have no side effects if it fails. This is easy if the failure happens before anything is modified in the first place, but can also be achieved by rolling back any changes so the program is returned to the pre-failure state.
- No throw / No fail guarantee -- The function will always succeed (no-fail) or fail without throwing an exception that is exposed to the caller (no-throw). Exceptions may be thrown internally if not exposed. The noexcept specifier maps to this level of exception safety guarantee.
上述是对异常处理的不同方式, 表示的是处理异常的不同的四种严格等级, 表示程序运行到异常处理时会进行的操作, 例如 Basic guarantee 只保证内存不会泄漏即可, 以及Strong guarantee保证异常发生的时候, 程序可以回退到正常的状态.
Let’s look at the no-throw/no-fail guarantees in more detail:
The no-throw guarantee: if a function fails, then it won’t throw an exception. Instead, it will return an error code or ignore the problem. No-throw guarantees are required during stack unwinding when an exception is already being handled; for example, all destructors should have a no-throw guarantee (as should any functions those destructors call). Examples of code that should be no-throw:
destructors and memory deallocation/cleanup functions
functions that higher-level no-throw functions need to call
The no-fail guarantee: a function will always succeed in what it tries to do (and thus never has a need to throw an exception, thus, no-fail is a slightly stronger form of no-throw). Examples of code that should be no-fail:
move constructors and move assignment (move semantics, covered in chapter 22)
swap functions
clear/erase/reset functions on containers
operations on std::unique_ptr (also covered in chapter 22)
functions that higher-level no-fail functions need to call
让我们查看关于 no-throw/no-fail guarantees 更多的细节, 这种方式保证内部不会出错, 或者出错的时候内部处理, 直接跳出程序, 不会再返回异常等待外部函数处理.