虽然一堆旧的坑还没有填完,但还是想开一个新的坑,讲下使用异常的正确姿势到底是什么,并且通过一些实际的例子,来让一些理论更加明确一些。错误处理(注意,这是讲的是通过的错误处理,不局限于使用异常处理错误)是那么的重要,但是工作日久,发现许多人的错误处理素养真的久佳。
TL;DR
如果不想看我的长篇大论的话,直接看下这个就可以了:
http://exceptionsafecode.com/exceptionsafecode.comInvariant
错误处理的第一个概念是Invariant(不变式),它是个啥呢?一个对象应该满足的一致性约束,即为此对象的invariant。举例来说,你实现一个基于红黑树的map类,有两个成员:
class map {
size_t _size;
node_t _root;
};
它的invariant即为,_root所索引的树的node的数量,应该与_size相等。
再举个例子,你实现一个动态分配的vector:
class vector {
T* _paylaod;
size_t _size;
size_t _capacity;
};
其中,_payload是实际的存储空间,_size是当前已经使用了的空间,_capacity是存储空间的实例长度,[_payload, _payload + _size)这块空间上,应该有当前此vector所存储的所有有效对象,刚刚这些描述,其实就是任意一个vector实例的invariant,否则,你的程序的正确性就无法保证了。
对于一个对象的invariant,我们需要了解以下几点:
- 对象构造完成后,invariant一定成立
- 一个对象的方法执行前,对象invariant一定成立
- 一个对象的方法执行后,对象invariant一定成立
- 一个对象的方法执行中,对象invariant可能暂时被违反,例子:
void vector::push_back(const T& t)
{
// a. invariant holds
_payload[_size] = t;
// b. invariant breaks
++_size;
// c. invariant holds
}
注意这个函数是有问题的,但是并不妨碍我们解释问题:
- a位置上,即函数执行前,invariant成立
- c位置上,即函数执行后,invariant成立
- 但是b位置上,即函数执行中,invariant暂时被破坏了
那么问题来了,如果对象的invariant暂时被破坏时,此时出现了一个error,对象的invariant会怎样?这直接关系到程序的正确性。
Exception safety
Exception safety用于描述,当一个对象的方法执行期间,如果出错,这个对象的invariant会怎样。注意,这个名词是exception safety,但是将其理解为error safety,并无差别。
- no guarantee:对象invariant不再成立,或者存在资源泄漏,可能会出现任何情况
- basic guarantee:对象invariant成立,但是我们不知道它具体的状态是啥,此时,这个对象不能再被使用,只能reset成原始状态,或者销毁。
- strong guarantee:对象invariant成立,且状态与调用方法之前完全相同,即这个失败的操作,没有对这个对象产生任何效果。
- no except:此操作保证一定成功,保证不会抛出任何异常
上面的名词可能会让你有些不知所云,我们不妨回归本质:错误处理,到底是在处理些啥?
- 保证对象的方法执行出现错误后,对象的invariant仍然成立(因为如果不成立,程序逻辑就不对了)
- 无任何资源泄漏
- 向上层上报这个错误
basic guarantee/basic guarantee/basic guarantee都是第一点、第二点的体现,只不过程度上有不同。对于第三点,这是最简单的,我们直接thow一个异常,或者return一个error code就可以了。
下面举一个错误的示例(这个例子是生造的,所以会显得有些怪,但是并不影响我们阐释概念),解释no guarantee,为了不失一般性,我们使用error code作为错误汇报机制。
struct Configuration {
size_t _len_of_content {}
char* _content {};
// no guarantee
int load(const char* file)
{
FILE* f = std::fopen(file, "r");
int r = read_int(f, &_len_of_content);
if (r != 0) {
return r;
}
_content = malloc(_len_of_content);
if (!_content) {
return r;
}
r = read_string(f, &_content, _len_of_content);
if (r != 0) {
return r;
}
std::fclose(f);
return r;
}
};
Configuration::load即为no guarantee,因为异常时,存储资源泄漏,并且,_len_of_content与_content实际上不一致了。我们可以将其改一下,进行一些错误处理,来将其做成basic guarantee。
struct Configuration {
size_t _len_of_content {}
char* _content {};
// no guarantee
int load(const char* file)
{
FILE* f = std::fopen(file, "r");
int r = read_int(f, &_len_of_content);
if (r != 0) {
std::fclose(f);
_len_of_content = 0;
return r;
}
_content = std::malloc(_len_of_content);
if (!_content) {
std::fclose(f);
_len_of_content = 0;
return -1;
}
r = read_string(f, &_content, _len_of_content);
if (r != 0) {
std::fclose(f);
return r;
}
std::fclose(f);
return r;
}
};
现在,此函数提供了basic guarantee,我们没有资源泄漏了,也没有不一致了(_len_of_content一定与_content的真正长度相等)。
但我们可以做得更好,实现strong gurantee,即,出错时,整个对象的状态不会有任何修改。
struct Configuration {
size_t _len_of_content {}
char* _content {};
// no guarantee
int load(const char* file)
{
FILE* f = std::fopen(file, "r");
size_t len = 0;
char* content = nullptr;
int r = read_int(f, &len);
if (!r) goto cleanup;
content = malloc(len);
if (!content) {
r = -ENOMEM;
goto cleanup;
}
r = read_string(f, &content, len);
if (r != 0) {
goto cleanup;
}
_len_of_content = len;
_content = content
content = nullptr;
cleanup:
std::fclose(f);
std::free(content);
return r;
}
};
这是我们可以做的到最好程序了,noexcept是肯定做不到的了。
Execution Root
前面提到,异常处理的第一个目的是让我们的程序的invariant可以恢复,这样,它可以安全地忽略这个异常,继续执行。
然后异常不能发生过就完了,我们需要报告此异常,无法是返回给终端用户,还是记录一条log。也即异常处理的第三个目的,上报异常。
一般的,如果代码写得好,几乎所有函数中都不会出现try/catch。然而,我们总终需要在一个地方进行try/catch,然后在catch中报告我们的异常。否则,我们的程序就crash掉了(未处理的异常会导致std::terminate被调用)。
那么,我们在什么地方进行catch呢?答案是:Execution Root。
- main:如果不处理,一直向上传,最终会terminate整个程序
- 线程的主函数:如果不处理,一直向上传,最终会terminate整个程序
- 处理用户请求的根函数:比如
void XXXRPCService::Echo(RPCRequest* req, RPCResponse* resp)
try {
// do it
} catch (timed_out_error&) {
resp->Fail(ERROR_TIMED_OUT);
} catch (server_busy_error&) {
resp->Fail(ERROR_SERVER_BUSY);
} catch (std::bad_alloc&) {
resp->Fail(ERROR_NO_MEM);
} catch (std::exception& e) {
resp->Fail(ERROR_UNKNWON);
}
错误处理的实质是,我们在出错时,进行一些处理,保证:
- 对象invariant不被破坏
- 无资源泄漏(C++中会使用独立的机制来管理资源,因此,异常处理根据不需要管资源问题)
然后,(可选的),将错误上报告或者记录。
Exception safety描述了出错时,对象invariant的具体状态,它决定着,当前对象是否仍然可用,以及可以怎么用,我们需要通过异常处理,来保证exception safety:
- no guarantee:绝对不允许,出错时,我们必须要进行一些处理,保证无资源泄漏,对象仍然处于可reset/可destroy的状态。
- basic guarantee:最低要求,对象invariant成立,但具体状态未知,只允许reset/destroy
- strong guarantee:如果能以一种低成本的方式做到,那就提供strong guarantee,否则,basic guarantee就够了。
- noexcept:对于某些状态的操作,必须提供此保证,否则,我们无法写出正确且优雅的错误处理代码(之后会具体阐释)。