【问题标题】:Is destroying the last std::exception_ptr pointing to an exception after calling std::rethrow_exception portable?在调用 std::rethrow_exception 可移植后销毁指向异常的最后一个 std::exception_ptr 吗?
【发布时间】:2017-10-03 15:08:57
【问题描述】:

我有一个关于异常生命周期的可移植性问题。在下面的代码中,在一个线程 (mySlave) 中引发异常并使用 std::exception_ptr 将异常转移到另一个线程 (myMaster)。 myMaster 总是通过std::condition_variable 等待不同的事件。 mySlave 中的一个例外就是这样的事件。在 myMaster 的 wait 的谓词函数中,我检查异常指针是否为空。如果在 mySlave 中抛出了异常,我将异常指针复制到 myMaster 中的一个临时变量,将原始异常指针设置为 null 并在 myMaster 中重新抛出它。这样,一旦程序从异常中恢复,原始异常指针就可以在谓词函数中服务了。

这在 VC14 上运行良好,但最终的软件很可能在未来被移植到其他平台。在我的代码中,所有对异常的exception_ptr 引用将在重新抛出后超出范围,因此原始异常将被销毁。我担心std::rethrow_exception 是否保证在重新抛出异常时始终生成异常的副本,或者它是否也可以使用对异常的引用,导致当我尝试在 myMaster 中捕获 if 时它不再有效?

#include <mutex>
#include <thread>
#include <atomic>
#include <exception>
#include <iostream>
#include <memory>

class SomeClass
{
public:
    /*...*/
    void MaseterFunction();
    void SlaveFunction();

private:
    /*...*/
    std::mutex mutex_gotEvent;
    std::condition_variable condVar_gotEvent;
    std::exception_ptr slaveLoopException;
    /*...*/
    std::atomic<bool> running = true;
};

class MyException : public std::runtime_error
{
public:
    MyException() : std::runtime_error("Ooops") {}
};

void SomeClass::SlaveFunction()
{
    try
    {
        throw MyException();
    }catch(const std::exception& e)
    {
        std::unique_lock<std::mutex> lock(mutex_gotEvent);
        slaveLoopException = std::current_exception();
        condVar_gotEvent.notify_all();
    }
}


void SomeClass::MaseterFunction()
{
    while (running)
    {
        try
        {
            {
                /*Wait for something interesting to happen*/
                std::unique_lock<std::mutex> lock(mutex_gotEvent);
                condVar_gotEvent.wait(lock, [=]()->bool {
                    return !(slaveLoopException == nullptr); // Real code waits for several events 
                });
            }

            /*Care for events*/
            /*...*/
            if (slaveLoopException)
            {
                std::exception_ptr temp_ptr = slaveLoopException;
                slaveLoopException = nullptr;

                std::rethrow_exception(temp_ptr);
            }
        }
        catch (const MyException& e)
        {
            std::cout << e.what();
            running = false;
        }
    }
}

int main()
{
    std::shared_ptr<SomeClass> someClass = std::make_shared<SomeClass>();
    std::thread myMaster([someClass]() {someClass->MaseterFunction(); });
    std::thread mySlave([someClass]() {someClass->SlaveFunction(); });

    std::cin.ignore();

    if (myMaster.joinable())
    {
        myMaster.join();
    }
    if (mySlave.joinable())
    {
        mySlave.join();
    }
    return 0;
}

我考虑过在类级别声明temp_ptr 或使用std::atomic&lt;bool&gt; 变量以及要在谓词函数中使用的异常指针。然而,这两种解决方案都会在不再使用异常后保持异常状态,这对我来说似乎不是很优雅。也可以在 myMaster 中的每个 catch 块中将异常指针设置为 null,但我认为这可能会在以后添加新异常并且程序员忘记将异常指针为 null 时引入错误。

编辑:

我找到了关于这个主题的以下陈述:

Statement 1

std::exception_ptr 引用的异常对象仍然有效 只要还有至少一个 std::exception_ptr 是 引用它

Statement 2:

rethrow_exception 的 VS 实现似乎复制了 例外。 Clang 和 gcc 不会复制。

Statement 3

异常对象在最后一个剩余之后被销毁 异常的活动处理程序以任何方式退出 重新抛出,或 std::exception_ptr 类型的最后一个对象(第 18.8.5 节) 指异常对象被销毁,以较晚者为准。

从 (1) 开始,我预计异常会过早地被销毁。从 (2) 开始,我希望这在使用 VC 作为副本时无效。从 3 开始,我不知道这是否可以在使用 gcc 或 clang 时拯救我。我的意思是,退出是通过重新抛出来实现的,但它不是从处理程序中重新抛出的。当临时指针被破坏时,新的处理程序是否已经被认为是活动的,或者指针首先被破坏并伴随着异常,从而使后续 catch 块具有无效的异常引用?

【问题讨论】:

    标签: c++ c++11 exception-handling language-lawyer


    【解决方案1】:

    CppRef 声明 rethrow_exception -- 抛出之前捕获的异常对象。我自己没有检查过标准(但见下文),但是:

    推测

    在我看来,从你重新抛出异常开始,异常处理就“恢复正常”了,也就是说,实现的工作就是保持抛出的对象,而语言并不关心无论您是通过普通的throw 还是通过rethrow_exception 投掷。

    换一种说法:exception_ptr 需要在rethrow_exception 处有效,之后抛出的异常与其他异常相同,是否与原始异常指针共享无关紧要。


    OP 提供了一个很好的link 并附有标准报价:

    4/ 异常对象的内存以未指定的方式分配,除非在 §3.7.4.1 中注明。如果一个处理程序通过重新抛出而退出,则控制权将传递给另一个处理程序以处理相同的异常。 在异常的最后一个剩余活动处理程序以除重新抛出以外的任何方式退出后,或者引用异常对象的std::exception_ptr(§18.8.5)类型的最后一个对象被销毁后,异常对象被销毁,以较晚者为准。 在前一种情况下,销毁发生在处理程序退出时,在处理程序中 exception-declaration 中声明的对象(如果有)销毁之后立即发生。在后一种情况下,销毁发生在std::exception_ptr 的析构函数返回之前。然后实现可以为异常对象释放内存;任何此类解除分配都是以未指定的方式完成的。 [注意:抛出的异常不会传播到其他线程,除非使用适当的库函数捕获、存储和重新抛出;见§18.8.5 和§30.6。 ——尾注 ]

    这似乎暗示了我上面写的内容:异常对象只要必要就存在:引用的 whichever is later 部分似乎明确指出了这一点。

    更新:我错过了 OP 不会从处理程序重新抛出,所以我也不确定这里应该发生什么。

    确实相信,在rethrow_exception 之后,飞行中的(新)异常应该被视为是由正常的throw 表达式生成的——其他任何事情都没有意义。

    推测的方式是,即使在std::exception_ptr 之前,每个实现都有一个类似于std::exception_ptr 的内部机制,因为它必须保留抛出的异常对象(复制或不复制) ) 在正常堆栈帧之外 处于活动状态,直到堆栈上的任何处理程序不再需要它为止。所以我可以推测,clang 或 gcc 不会进行复制,而是在您调用 throw/rethrow_exception 时立即保存一个“内部异常_ptr”。

    【讨论】:

    猜你喜欢
    • 2022-01-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-08
    • 1970-01-01
    • 2013-09-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多