【问题标题】:C++: In throwing/catching exceptions, when are the exception objects destructed?C++:在抛出/捕获异常时,异常对象何时被破坏?
【发布时间】:2021-05-08 08:48:51
【问题描述】:

我看到了这篇文章:Best practice: throw by value, catch by const reference,并对异常对象何时被破坏感到好奇。

这是我的异常结构,就像文章中的第二个示例一样(带有一些 std::cout 的)。

struct BASE_EX {
    static int id;
    BASE_EX() { 
        ++id; 
        std::cout << "constructing BASE_EX " << id << std::endl; 
    }
    virtual std::string const what() const { return "BASE_EX " + std::to_string(id); }
    ~BASE_EX() { std::cout << "destructing BASE_EX " << id << std::endl; }
};

struct DERIVED_EX : BASE_EX {
    static int derived_id;
    DERIVED_EX() {
        ++derived_id; 
        std::cout << "constructing DERIVED_EX " << derived_id << std::endl; 
    }
    std::string const what() const { return "DERIVED_EX " + std::to_string(derived_id); }
    ~DERIVED_EX() { std::cout << "destructing DERIVED_EX " << derived_id << std::endl; }
};

int BASE_EX::id = 0;
int DERIVED_EX::derived_id = 0;

运行这个主函数时,通过 const& 捕捉:

int main() {
    try {
        try {
            throw DERIVED_EX();
        } catch(BASE_EX const& ex) {
            std::cout << "First catch block: " << ex.what() << std::endl;
            throw ex;
        }
    } catch(BASE_EX const& ex) {
        std::cout << "Second catch block: " << ex.what() << std::endl;
    }
}

我明白了

constructing BASE_EX 1
constructing DERIVED_EX 1
First catch block: DERIVED_EX 1
destructing DERIVED_EX 1
destructing BASE_EX 1
Second catch block: BASE_EX 1
destructing BASE_EX 1

问题一:如果BASE_EX在第二次catch之前就被破坏了,它是如何被捕捉到的?

问题 2:为什么破坏比构建多?

问题3:当我将两个catch都改为catch by value而不是const&时,为什么输出变成了

constructing BASE_EX 1
constructing DERIVED_EX 1
First catch block: BASE_EX 1
destructing BASE_EX 1
destructing DERIVED_EX 1
destructing BASE_EX 1
Second catch block: BASE_EX 1
destructing BASE_EX 1
destructing BASE_EX 1

任何关于 cpp try-catch 如何在后台工作的推荐读物都会很棒。谢谢。

【问题讨论】:

  • 该异常在概念上在它通过的每个块的末尾被破坏,并传递一个副本。然而,编译器可能(并且在实践中,为了效率通常会这样做)省略一些 - 但重要的是,不一定是所有 - 副本。如果异常最终被捕获,则它在相关捕获处理程序的末尾不再存在。如果未捕获到异常,则程序以调用std:abort() 结束,而不必调用析构函数。如果您跟踪复制(或移动?)构造函数的调用,您可能会更好地了解正在发生的事情。
  • “如果 [object] 在 [X] 之前被破坏,那么它 [still around] 是怎么回事?” -- 这种形式的问题经常出现在人们试图跟踪建设和破坏,但忽略Rule of Three。让我们看看......是的,就是这样。
  • 谢谢@Peter 和 JaMiT。我认为 JaMiT 共享的两个链接回答了我的问题。

标签: c++ exception destructor


【解决方案1】:

了解它们何时被销毁的最好方法是假装catch 类是一个函数:

catch(BASE_EX const& ex) {
    std::cout << "Second catch block: " << ex.what() << std::endl;
}

临时调整一下,假装这是一个函数,ex只是这个函数的一个参数:

void exception_handler(BASE_EX const& ex) {
    std::cout << "Second catch block: " << ex.what() << std::endl;
}

只要这个伪函数参数被破坏,异常对象就会被破坏,如果输入异常处理程序,就好像它是一个普通的函数调用一样,在这里。

【讨论】:

    【解决方案2】:

    如果你要标记一个对象的构造函数,标记 all ;)

    struct BASE_EX {
        static int count;
        int id;
        BASE_EX() : id(count++) { 
            std::cout << "constructing BASE_EX " << id << std::endl; // usually std::endl is unnecessary (it's just "\n" followed by std::flush), but since we're playing with crashes it's probably a good idea
        }
        BASE_EX(BASE_EX const &other) : id(count++) {
            std::cout << "copying BASE_EX " << other.id << " as BASE_EX " << id << std::endl;
        }
        // implicit move constructor not declared
        virtual std::string what() const { return "BASE_EX " + std::to_string(id); } // marking by-value return as const does absolutely nothing
        ~BASE_EX() { std::cout << "destructing BASE_EX " << id << std::endl; } // reminder that base class destructors should generally be virtual; not required in this case
    };
    int BASE_EX::count = 0;
    
    struct DERIVED_EX : BASE_EX {
        static int count;
        int id;
        DERIVED_EX() : BASE_EX(), id(count++) {
            std::cout << "constructing DERIVED_EX " << id << std::endl; 
        }
        DERIVED_EX(DERIVED_EX const &other) : BASE_EX(other), id(count++) {
            std::cout << "copying DERIVED_EX " << other.id << " as DERIVED_EX " << id << std::endl;
        }
        // implicit move constructor not declared
        std::string what() const override { return "DERIVED_EX " + std::to_string(id); }
        ~DERIVED_EX() { std::cout << "destructing DERIVED_EX " << id << std::endl; }
    };
    int DERIVED_EX::count = 0;
    

    你得到

    constructing BASE_EX 0
    constructing DERIVED_EX 0
    First catch block: DERIVED_EX 0
    copying BASE_EX 0 as BASE_EX 1
    destructing DERIVED_EX 0
    destructing BASE_EX 0
    Second catch block: BASE_EX 1
    destructing BASE_EX 1
    

    第一个throw 将异常对象设置为DERIVED_EX 0。内部catch 获取对该异常对象的BASE_EX 0 基类子对象的引用。因为whatvirtual,调用它会导致DERIVED_EX 报告它的类型。但是,当你再次throw ex 时,ex 只有静态类型BASE_EX,所以新的异常对象被选为BASE_EX,它是通过复制只复制BASE_EX 部分 的第一个异常对象。当我们退出第一个 catch 时,第一个异常对象被销毁,外部 catch 接收新的 BASE_EX 对象。因为它确实BASE_EX,而不是DERIVED_EX,调用what 反映了这一点。如果您将两个 catchs 都设为按值,您会得到

    constructing BASE_EX 0
    constructing DERIVED_EX 0
    copying BASE_EX 0 as BASE_EX 1
    First catch block: BASE_EX 1
    copying BASE_EX 1 as BASE_EX 2
    destructing BASE_EX 1
    destructing DERIVED_EX 0
    destructing BASE_EX 0
    copying BASE_EX 2 as BASE_EX 3
    Second catch block: BASE_EX 3
    destructing BASE_EX 3
    destructing BASE_EX 2
    

    当你catch按值时,异常对象被复制来初始化catch参数。在执行这样的catch 块期间,有两个对象表示异常:没有名称的实际异常对象,以及为catch 块制作的它的副本,可以命名。第一个副本是将第一个异常对象复制到第一个catch 的参数。第二个副本是该参数作为第二个异常对象的副本。第三个是将异常对象复制到第二个catch 的参数中。在我们输入第一个catch 时,异常的DERIVED_EX 部分已被删除。 catch 参数在每个 catch 结束时被销毁,按照通常的范围规则。只要相应的catch 块退出,异常对象就会被销毁。

    您可以通过不按值获取异常并且不使用throw &lt;catch-parameter&gt; 重新抛出异常来避免复制问题和切片问题。

    int main() {
        try {
            try {
                throw DERIVED_EX();
            } catch(BASE_EX const &ex) {
                std::cout << "First catch block: " << ex.what() << std::endl;
                throw;
            }
        } catch(BASE_EX const &ex) {
            std::cout << "Second catch block: " << ex.what() << std::endl;
        }
    }
    

    给予

    constructing BASE_EX 0
    constructing DERIVED_EX 0
    First catch block: DERIVED_EX 0
    Second catch block: DERIVED_EX 0
    destructing DERIVED_EX 0
    destructing BASE_EX 0
    

    异常对象不会在第一个catch 结束时被销毁,因为它以throw 退出,这表明相同 异常对象将用于匹配更多catch 子句.它不会像throw ex 所要求的那样被复制到新的异常对象中并被销毁。

    有关规则的详细说明,请参阅cppreference

    【讨论】:

      猜你喜欢
      • 2016-02-17
      • 2013-06-24
      • 1970-01-01
      • 2013-05-24
      • 2012-04-26
      • 2017-02-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多