从C`goto`错误处理范例转换到C++异常处理范例

Vil*_*ray 24 c c++ error-handling exception-handling exception

我是一名学习C++的C程序员.在C中,有一个常用的goto习惯用法来处理错误并从函数中干净地退出.我已经读过,在面向对象的程序中,通过try- catchblocks的异常处理是首选,但我在使用C++实现这个范例时遇到了麻烦.

以C中的以下函数为例,它使用了goto错误处理范例:

unsigned foobar(void){
    FILE *fp = fopen("blah.txt", "r");
    if(!fp){
        goto exit_fopen;
    }

    /* the blackbox function performs various
     * operations on, and otherwise modifies,
     * the state of external data structures */
    if(blackbox()){
        goto exit_blackbox;
    }

    const size_t NUM_DATUM = 42;
    unsigned long *data = malloc(NUM_DATUM*sizeof(*data));
    if(!data){
        goto exit_data;
    }

    for(size_t i = 0; i < NUM_DATUM; i++){
        char buffer[256] = "";
        if(!fgets(buffer, sizeof(buffer), fp)){
            goto exit_read;
        }

        data[i] = strtoul(buffer, NULL, 0);
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
        printf("%lu\n", data[i] + data[i + NUM_DATUM/2]);
    }

    free(data)
    /* the undo_blackbox function reverts the
     * changes made by the blackbox function */
    undo_blackbox();
    fclose(fp);

    return 0;

exit_read:
    free(data);
exit_data:
    undo_blackbox();
exit_blackbox:
    fclose(fp);
exit_fopen:
    return 1;
}
Run Code Online (Sandbox Code Playgroud)

我尝试使用异常处理范例在C++中重新创建函数:

unsigned foobar(){
    ifstream fp ("blah.txt");
    if(!fp.is_open()){
        return 1;
    }

    try{
        // the blackbox function performs various
        // operations on, and otherwise modifies,
        // the state of external data structures
        blackbox();
    }catch(...){
        fp.close();
        return 1;
    }

    const size_t NUM_DATUM = 42;
    unsigned long *data;
    try{
        data = new unsigned long [NUM_DATUM];
    }catch(...){
        // the undo_blackbox function reverts the
        // changes made by the blackbox function
        undo_blackbox();
        fp.close();
        return 1;
    }

    for(size_t i = 0; i < NUM_DATUM; i++){
        string buffer;
        if(!getline(fp, buffer)){
            delete[] data;
            undo_blackbox();
            fp.close();
            return 1;
        }

        stringstream(buffer) >> data[i];
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
        cout << data[i] + data[i + NUM_DATUM/2] << endl;
    }

    delete[] data;
    undo_blackbox();
    fp.close();

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我觉得我的C++版本没有正确实现异常处理范例; 实际上,由于catch随着函数的增长在块中积累了清理代码,C++版本似乎更不易读,更容易出错.

我已经读过,由于RAII之类的东西,在C++中可能不需要catch块中的所有清理代码,但我对这个概念并不熟悉.我的实现是否正确,或者是否有更好的方法来处理错误并在C++中干净地退出函数?

Mik*_*our 31

RAII的原则是你使用类类型来管理任何需要在使用后清理的资源; 清理是由析构函数完成的.

这意味着您可以创建一个本地RAII管理器,当它超出范围时,它将自动清理它正在管理的任何内容,无论是由于正常的程序流还是异常.绝不应该只需要一个catch块来清理; 只有当您需要处理或报告异常时.

在您的情况下,您有三个资源:

  • 该文件fp.ifstream已经是RAII类型,所以只需删除冗余调用fp.close(),一切都很好.
  • 分配的内存data.如果它是一个小的固定大小(std::vector如果是这样),或者需要动态分配,则使用本地数组; 然后摆脱了delete.
  • 国家成立blackbox.

您可以为"黑匣子"malarkey编写自己的RAII包装器:

struct blackbox_guard {
    // Set up the state on construction
    blackbox_guard()  {blackbox();}

    // Restore the state on destruction
    ~blackbox_guard() {undo_blackbox();}

    // Prevent copying per the Rule of Three
    blackbox_guard(blackbox_guard const &) = delete;
    void operator=(blackbox_guard) = delete;
};
Run Code Online (Sandbox Code Playgroud)

现在您可以删除所有错误处理代码; 我通过异常(抛出或允许传播)而不是魔术返回值指示失败,给出:

void foobar(){
    ifstream fp ("blah.txt"); // No need to check now, the first read will fail if not open
    blackbox_guard bb;

    const size_t NUM_DATUM = 42;
    unsigned long data[NUM_DATUM];   // or vector<unsigned long> data(NUM_DATUM);

    for(size_t i = 0; i < NUM_DATUM; i++){
        string buffer;

        // You could avoid this check by setting the file to throw on error
        // fp.exceptions(ios::badbit); or something like that before the loop
        if(!getline(fp, buffer)){
             throw std::runtime_error("Failed to read"); // or whatever
        }

        stringstream(buffer) >> data[i]; // or data[i] = stoul(buffer);
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
        cout << data[i] + data[i + NUM_DATUM/2] << endl;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @doynax:根据我的经验,它是大型应用程序最可靠的模型 - 您不能忽略异常,或忘记清理资源.如果您没有正确使用RAII,那么隐式控制路径只是一个问题; 也就是说,如果您使用的类型不至少提供[基本例外保证](http://en.wikipedia.org/wiki/Exception_safety).一致地使用RAII,例外将是你的朋友.不要,并且非异常流量(返回,中断等)也可能是一个问题,尽管比异常更明显. (13认同)
  • @Mgetz:它仍然是正则性的三级规则(如果你想让它们与复制语义不同,你只需要担心移动语义).删除复制操作也会删除移动操作,因此我的包装器既不可复制也不可移动. (8认同)
  • 这无疑是一个错误的论坛,但异常/ RAII模型在大型应用程序中的实践往往如何发挥作用?我最后一次尝试它时,我倾向于忘记隐式代码路径,并在罕见的异常后使系统进入不一致状态.另一方面,我也常常知道手动清理和错误情况测试是错误的. (2认同)
  • @ChrisDrew:事实上,RAII并没有强制使用例外.我更喜欢异常,因为返回值可以(并且在我的经验中有时会被忽略),导致异常会立即暴露的微妙错误; 其他人有其他意见.但问题是如何使用RAII,而不是是否使用例外. (2认同)
  • @doynax:是的,在进行状态更改时,您需要[强异常保证](http://en.wikipedia.org/wiki/Exception_safety).我建议避免可变状态,但我们已经离得很远了. (2认同)

Rei*_*ica 10

是的,您应尽可能使用RAII(资源获取初始化).它导致代码易于阅读安全.

您的核心思想是在初始化对象期间获取资源,并设置对象以便正确释放资源以进行销毁.这一点至关重要的一点是,当范围通过异常退出时,析构函数会正常运行.

在您的情况下,已经有RAII可用,您只是没有使用它.std::ifstream(我认为这是你ifstream所指的)确实在破坏时关闭.所以所有的close()呼叫都catch可以安全地省略,并且会自动发生 - 正是RAII的用途.

因为data,您也应该使用RAII包装器.有两种可用的:std::unique_ptr<unsigned long[]>std::vector<unsigned long>.两者都在各自的析构函数中处理内存释放.

最后,blackbox()您可以自己创建一个简单的RAII包装器:

struct BlackBoxer
{
  BlackBoxer()
  {
    blackbox();
  }

  ~BlackBoxer()
  {
    undo_blackbox();
  }
};
Run Code Online (Sandbox Code Playgroud)

当用这些重写时,您的代码将变得更加简单:

unsigned foobar() {
  ifstream fp ("blah.txt");
  if(!fp.is_open()){
    return 1;
  }

  try {
    BlackBoxer b;

    const size_t NUM_DATUM = 42;
    std::vector<unsigned long> data(NUM_DATUM);
    for(size_t i = 0; i < NUM_DATUM; i++){
      string buffer;
      if(!getline(fp, buffer)){
        return 1;
      }

      stringstream(buffer) >> data[i];
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
      cout << data[i] + data[i + NUM_DATUM/2] << endl;
    }

    return 0;
  } catch (...) {
    return 1;
  }
}
Run Code Online (Sandbox Code Playgroud)

另外,请注意您的函数使用返回值来指示成功或失败.这可能是你想要的(如果这个函数的失败是"正常的"),或者可能只代表中途(如果失败也应该是特殊的).

如果是后者,只需将函数更改为void,删除try- catch构造,并抛出一个合适的异常而不是return 1;.

最后,即使您决定保持返回值方法(这是完全有效的),也可以考虑将函数更改为返回bool,true意味着成功.这更具惯用性.


Mge*_*etz 7

让我用你的c ++习惯用法重写一下,内联代码解释

// void return type, we may no guarantees about exceptions
// this function may throw
void foobar(){
   // the blackbox function performs various
   // operations on, and otherwise modifies,
   // the state of external data structures
   blackbox();

   // scope exit will cleanup blackbox no matter what happens
   // a scope exit like this one should always be used
   // immediately after the resource that it is guarding is
   // taken.
   // but if you find yourself using this in multiple places
   // wrapping blackbox in a dedicated wrapper is a good idea
   BOOST_SCOPE_EXIT[]{
       undo_blackbox();
   }BOOST_SCOPE_EXIT_END


   const size_t NUM_DATUM = 42;
   // using a vector the data will always be freed
   std::vector<unsigned long> data;
   // prevent multiple allocations by reserving what we expect to use
   data.reserve(NUM_DATUM);
   unsigned long d;
   size_t count = 0;
   // never declare things before you're just about to use them
   // doing so means paying no cost for construction and
   // destruction if something above fails
   ifstream fp ("blah.txt");
   // no need for a stringstream we can check to see if the
   // file open succeeded and if the operation succeeded
   // by just getting the truthy answer from the input operation
   while(fp >> d && count < NUM_DATUM)
   {
       // places the item at the back of the vector directly
       // this may also expand the vector but we have already
       // reserved the space so that shouldn't happen
       data.emplace_back(d);
       ++count;
   }

   for(size_t i = 0; i < NUM_DATUM/2; i++){
       cout << data[i] + data[i + NUM_DATUM/2] << endl;
   }
}
Run Code Online (Sandbox Code Playgroud)

c ++最强大的功能不是类,它是析构函数.析构函数允许在退出范围时释放或释放资源或职责.这意味着您不必多次重写清理代码.而且因为只能破坏构造的物体; 如果你从来没有找到一件物品从而没有建造它,那么如果发生了什么事,你就不会在破坏中支付任何罚款.

如果你发现自己重复清理代码,那应该是一个标志,有问题的代码没有利用析构函数和RAII的强大功能.


Chr*_*ckl 7

在C中,有一个常见的goto习惯用于处理错误和从函数退出清除.我已经读过在面向对象的程序中首选通过try-catch块进行异常处理,

对于C++来说,这根本不是真的.

但是C++具有确定性析构函数而不是finally块(例如,在Java中使用),这是错误处理代码的游戏改变者.

我已经读过,由于名为RAII的东西,在C++中可能不需要catch块中的所有清理代码,

是的,在C++中你使用"RAII".对于伟大的概念来说,这是一个糟糕的名字.这个名字很差,因为它强调i nitialisation(资源获取是初始化).相比之下,RAII的重要之处在于破坏.由于本地对象的析构函数将在块的末尾执行,无论发生什么,无论是早期返回还是异常,它都是发布资源的代码的理想之地.

但我不熟悉这个概念.

那么,从一开始,您就可以从维基百科的定义开始:

http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

或者你直接去Bjarne Stroustrup的网站:

http://www.stroustrup.com/bs_faq2.html#finally

我相信我们非常乐意回答有关使用它的成语或问题的特定方面的问题:)

我的实现是否正确,或者是否有更好的方法来处理错误并在C++中干净地退出函数?

您的实现不是人们对优秀C++代码的期望.

以下是使用RAII的示例.它使用异常来报告错误,并使用析构函数来执行清理操作.

#include <fstream>
#include <stdexcept>
#include <vector>

// C or low-level functions to be wrapped:
int blackbox();
void undo_blackbox();

// just to be able to compile this example:
FILE *fp;

// The only self-made RAII class we need for this example
struct Blackbox {
    Blackbox() {
        if (!blackbox()) {
            throw std::runtime_error("blackbox failed");
        }
    }

    // Destructor performs cleanup:
    ~Blackbox() {
        undo_blackbox();
    }   
};

void foobar(void){
    // std::ifstream is an implementation of the RAII idiom,
    // because its destructor closes the file:
    std::ifstream is("blah.txt");
    if (!is) {
        throw std::runtime_error("could not open blah.txt");
    }

    Blackbox local_blackbox;

    // std::vector itself is an implementation of the RAII idiom,
    // because its destructor frees any allocated data:
    std::vector<unsigned long> data(42);

    for(size_t i = 0; i < data.size(); i++){
        char buffer[256] = "";
        if(!fgets(buffer, sizeof(buffer), fp)){
            throw std::runtime_error("fgets error");
        }

        data[i] = strtoul(buffer, NULL, 0);
    }

    for(size_t i = 0; i < (data.size()/2); i++){
        printf("%lu\n", data[i] + data[i + (data.size()/2)]);
    }

    // nothing to do here - the destructors do all the work!
}
Run Code Online (Sandbox Code Playgroud)

顺便说一下,+1用于尝试用新语言学习新概念.用不同的语言改变你的心态并不容易!:)