【问题标题】:Transitioning from C `goto` error handling paradigm to C++ exception handling paradigm从 C `goto` 错误处理范例转换到 C++ 异常处理范例
【发布时间】:2015-04-22 06:16:00
【问题描述】:

我是一名学习 C++ 的 C 程序员。在 C 中,有一个常见的goto idiom used to handle errors and exit cleanly from a function。我读过在面向对象程序中首选通过try-catch 块处理异常,但我在用 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;
}

我尝试使用异常处理范例在 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;
}

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

我已经读到,由于称为 RAII 的东西,catch 块中的所有这些清理代码在 C++ 中可能是不必要的,但我不熟悉这个概念。 我的实现是否正确,还是有更好的方法来处理错误并干净地退出 C++ 中的函数?

【问题讨论】:

  • 如果data的大小是一个很小的编译时常数,为什么要动态分配它?如果您愿意,请使用不需要显式删除的std::vector,无论函数如何返回或抛出。您也不需要显式关闭文件流。将你的整个函数放在一个 try 块中,并根据一些逻辑(一个标志或捕获不同的异常类型)决定是否调用 undo_blackbox。
  • 明确查找 RAII。例如,您的fp.close() 是不必要的。使用 std::vector,您的数据删除也应该过时了。
  • RAII 的重点是:当blackbox() 抛出时,你不需要调用fp.close()(或者根本不需要,事实上)——ifstream 的析构函数会为你做这件事.使用scope guard 在异常退出时自动运行undo_blackbox()。将data 设为std::vector&lt;unsigned long&gt; - 无论函数正常退出还是异常退出,它的析构函数都会自动释放内存。一旦你通过 RAII 管理所有资源,你就很少会写 try/catch 块 - 你只会允许传播异常。
  • 在整个节目中实现 RAII 模式之前,我认为明智的做法是停下来问问自己“当未处理的异常向上传播时运行此清理代码是否合理?”这种模式一直让我觉得非常危险!如果您预料到异常,您会处理它。所以如果异常没有处理,很可能是意料之外的,所以你不知道自己程序的状态。在这种情况下运行更多代码真的明智吗?这就像一颗炸弹在午餐时引爆,在你撤离大楼之前,你要洗碗。
  • @EricLippert 好吧,唯一合理的做法是中止......您不会因为损坏的状态而抛出异常,当您的状态损坏时您会中止。未损坏但无效状态的例外情况(例如在没有空闲内存时分配内存;而不是在空闲块列表损坏时分配内存)。

标签: c++ c exception error-handling exception-handling


【解决方案1】:

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

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

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

  • 文件fpifstream 已经是 RAII 类型,因此只需删除对 fp.close() 的冗余调用即可。
  • 分配的内存data。如果它是一个小的固定大小(这样),则使用本地数组,如果需要动态分配,则使用std::vector;然后摆脱delete
  • blackbox 设置的状态。

您可以为“黑盒”恶意代码编写自己的 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;
};

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

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;
    }
}

【讨论】:

  • 这无疑是一个错误的论坛,但异常/RAII 模型在大型应用程序中如何发挥作用?上次尝试时,我倾向于忘记隐式代码路径,并在出现罕见异常后使系统进入不一致状态。另一方面,我也经常知道手动清理和错误条件测试是错误的。
  • @doynax:根据我的经验,它是大型应用程序最可靠的模型——你不能忽略异常,或者忘记清理资源。如果您一开始没有正确使用 RAII,那么隐式控制路径只会成为问题;也就是说,如果您使用的类型至少不提供basic exception guarantee。始终如一地使用 RAII,例外将成为您的朋友。不要这样做,非异常流(返回、中断等)也可能是一个问题,尽管它比异常更明显。
  • @Mgetz:正确性仍然是三法则(如果你想让它们与复制语义不同,你只需要担心移动语义)。删除复制操作也会删除移动操作,所以我的包装器既不可复制也不可移动。
  • @ChrisDrew:确实,RAII 并不强制使用异常。我更喜欢异常,因为返回值可以(并且根据我的经验有时会)被忽略,从而导致异常会立即暴露的细微错误;其他人有其他意见。但问题在于如何使用 RAII,而不是是否使用异常。
  • @doynax:是的,在进行状态更改时需要strong exception guarantee。我建议避免可变状态,但我们已经离题很远了。
【解决方案2】:

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

核心思想是在对象初始化期间获取资源,并设置对象以使其在销毁时正确释放资源。这样做的关键在于析构函数在通过异常退出作用域时正常运行。

在您的情况下,已经有 RAII 可用,而您只是没有使用它。 std::ifstream(我认为这就是你的ifstream 所指的)确实在销毁时关闭。因此,catch 中的所有 close() 调用都可以安全地省略,并且会自动发生——这正是 RAII 的用途。

对于data,您也应该使用 RAII 包装器。有两个可用:std::unique_ptr&lt;unsigned long[]&gt;std::vector&lt;unsigned long&gt;。两者都在各自的析构函数中处理内存释放。

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

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

  ~BlackBoxer()
  {
    undo_blackbox();
  }
};

用这些重写后,您的代码会变得更加简单:

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;
  }
}

此外,请注意您的函数使用返回值来指示成功或失败。这可能是您想要的(如果失败是此功能的“正常”),或者可能只是表示只进行了一半(如果失败也应该是异常的)。

如果是后者,只需将函数更改为void,摆脱trycatch 构造,并抛出合适的异常而不是return 1;

最后,即使您决定保留返回值方法(这是完全有效的),请考虑将函数更改为返回booltrue 表示成功。它更惯用。

【讨论】:

    【解决方案3】:

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

    C++ 完全不是这样。

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

    我了解到,catch 块中的所有这些清理代码可能是 由于称为 RAII 的东西,在 C++ 中是不必要的,

    是的,在 C++ 中,您使用“RAII”。对于一个伟大的概念来说,这是一个糟糕的名字。这个名字很糟糕,因为它强调i初始化(资源获取是初始化)。相比之下,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!
    }
    

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

    【讨论】:

    • 可能跑题了,但与 Java 相比,我真的很喜欢强调 C++ 中缺少 finally。其实这很明显,但我从来没有想过。为此 +1!
    【解决方案4】:

    让我使用 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;
       }
    }
    

    c++ 最强大的特性不是类,而是析构函数。析构函数允许在退出范围时释放或释放资源或职责。这意味着您不必多次重新编写清理代码。此外,因为只有构造的对象才能被破坏;如果你从来没有拿到过一件物品,因此从来没有建造过它,如果发生了什么事,你不需要支付任何销毁费用。

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

    【讨论】:

      猜你喜欢
      • 2011-08-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-08
      • 2017-10-11
      • 1970-01-01
      • 2011-07-14
      • 1970-01-01
      相关资源
      最近更新 更多