gdi+ 中发生一般性错误。_C++异常实战之一 错误处理到底是在处理些啥

虽然一堆旧的坑还没有填完,但还是想开一个新的坑,讲下使用异常的正确姿势到底是什么,并且通过一些实际的例子,来让一些理论更加明确一些。错误处理(注意,这是讲的是通过的错误处理,不局限于使用异常处理错误)是那么的重要,但是工作日久,发现许多人的错误处理素养真的久佳。

TL;DR

如果不想看我的长篇大论的话,直接看下这个就可以了:

http://exceptionsafecode.com/​exceptionsafecode.com

Invariant

错误处理的第一个概念是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:对于某些状态的操作,必须提供此保证,否则,我们无法写出正确且优雅的错误处理代码(之后会具体阐释)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值