5

我正在派生我自己的异常,调用它MyExceptionstd::system_error并已覆盖what()以计算并返回我的消息。MyException的初始值设定项列表不调用接受消息的 system_error 构造函数覆盖。

如果我抓住 aMyException并将其复制到 astd::exception调用iswhat()的结果。这是有道理的。std::exceptionnullptr

我的问题是,如果我确实使用了 system_exception 的构造函数,该构造函数在初始化时接收消息MyException,是否指定 system_error 将获取消息的副本并拥有它并释放它?

我假设这将使 的std::exception副本MyException能够返回有效的what(). 尽管我会因为每次MyExceptions创建新的“什么”都需要计算而受到性能影响;只有在第一次调用 what() 时,我才能懒惰地计算它。

我有点担心what()返回 achar*而不是 a的 'what' 字符串的所有权const std::string&

代码是这样的(我还没有编译过):

    class MyException : public std::system_error
    {
        std::string what_;
    public:
        MyException(int errorValue, const std::error_category& category)
            : std::system_error(errorValue, category)
        {}

        char* what() const
        {
           what_ = "MyException: " + to_string(code().value());
           return what_.c_str();
        }
    };

    int main()
    {
        std::exception ex;

        try
        {
            throw MyException(4, system_category());
        }
        catch( const MyException& e )
        {
            ex = e;
        }

        printf("what= %s", ex.what());

        return 1;
    }
4

3 回答 3

8

我的问题是,如果我确实使用了 system_exception 的构造函数,该构造函数在初始化时接收消息MyException,是否指定 system_error 将获取消息的副本并拥有它并释放它?

是的,这是由标准保证的。

开始,std::exception不拥有what——std::runtime_error确实。std::runtime_error的构造函数是这样定义的([runtime.error]p2-5):

runtime_error(const string& what_arg);

效果:构造一个类的对象runtime_error
后置条件: strcmp(what(), what_arg.c_str()) == 0

runtime_error(const char* what_arg);

效果:构造一个类的对象runtime_error
后置条件: strcmp(what(), what_arg) == 0

因此,它必须在内部存储一个副本what_arg因为对传入的值的生命周期没有要求。

接下来是[异常] p2:

T从类派生的每个标准库类exception都应具有可公开访问的复制构造函数和可公开访问的复制赋值运算符,它们不会因异常而退出。这些成员函数应满足以下后置条件:如果两个对象都具有动态类型lhs并且是的副本,则应等于。rhsTlhsrhsstrcmp(lhs.what(), rhs.what())0

因此,必须有一个复制构造函数,它绝不能抛出,并且副本必须保持相同的返回值what()。对于复制赋值运算符也是如此。

综上所述,我们可以推测std::runtime_error必须在引用计数字符串中保留您在what_arg内部传递的值(以避免复制时分配异常),并且无论复制和/或切片如何,该值都将持续存在 - 但仅限于std::runtime_error,下来std::exception!(有关 ' 存储的基本原理和要求的更多信息what可以在@HowardHinnant的这个非常有趣的答案中找到:移动 std::runtime_error 的构造函数

std::system_error继承自std::runtime_error,因此对于它和从它派生的任何类型都同样适用(只要派生类型保持非抛出复制构造函数不变)。

我假设这将使 的std::exception副本MyException能够返回有效的what().

不!当您制作 的std::exception 副本MyException,您将对象切分成what物理存储的值更少派生的类型。如果您必须复制您的异常,您可以使用的最小派生类型是std::runtime_error. (当然,您总是可以安全地std::exception 引用a MyException。)换句话说,永远不可能从std::exception 对象what().


此代码具有您想要的行为,可移植:

#include <cstdio>
#include <stdexcept>
#include <system_error>
#include <string>

class MyException : public std::system_error {
public:
    MyException(int errorValue, std::error_category const& category)
      : std::system_error(
            errorValue, category,
            "MyException: " + std::to_string(errorValue)
        )
    { }
};

int main() {
    std::runtime_error ex;

    try {
        throw MyException(4, system_category());
    } catch(MyException const& e) {
        ex = e;
    }

    std::printf("what= %s", ex.what());
}

我会说编写分配的异常构造函数的形式很糟糕(出于显而易见的原因),但鉴于我所知道的每个当前标准库实现都使用短字符串优化std::basic_string<>这极不可能成为问题在实践中。

于 2016-07-20T13:35:39.750 回答
2

您的问题与了解异常的生命周期有关。该问题在此处此处的帖子中进行了讨论,可能会有所帮助。

您可以保证使用智能指针延长异常的生命周期。我不确定性能影响是什么,但您可能会使用它来保留您自己的扩展std::system_error并完全避免复制构造。(实际上,我不保证会避免复制构造。似乎智能指针的创建可能会或可能不会复制异常。但它会复制您的异常,如果您提供复制构造函数,它应该做正确的事情你应该提供。)你的主要功能最终看起来更像这样。

#include <exception> // std::exception_ptr

int main()
{
    std::exception_ptr p;

    try
    {
        throw MyException(4, system_category());
    }
    catch( const MyException& e )
    {
        p = std::current_exception();
    }

    try
    {
        std::rethrow_exception(p);
    }
    catch (const std::exception& e)
    {
        printf("what= %s", e.what());
    }

    return 1;
}

这基本上只是对我在 cplusplus.com 上读到的使用异常指针的示例的重写但我使用的是您的异常类而不是标准异常std::logic_error

至于你原来的问题,似乎很难做出硬性保证。我确实在赋值运算符的文档中找到了适用于 C++11 的异常的以下语句。在 C++98 中甚至没有提供这种保证。

C++ 标准库中的每个异常(包括这个)至少都有一个复制赋值运算符重载,它保留了当动态类型匹配时成员返回的字符串表示形式。

但是,在您的情况下,std::system_error动态类型与动态类型不匹配std::exception,因此我认为不能保证它可以正常工作。

于 2016-07-20T11:43:28.983 回答
1

类异常不拥有任何字符串。当你切片你的异常对象时,你会得到一个基本的异常对象,它没有被覆盖的 what() 虚函数。

what() 函数的魔力在于虚函数 what() 并且在您的派生类中。您可以将存储在静态内存中的 const char * 传递给异常对象,并且不会被复制。

请注意,当您引发和异常时,对象的副本可能会引发新的异常并且不推荐使用(例如,在 bad_alloc 之后,您可能无法创建新的字符串对象)。这就是为什么通过引用而不是值更好地捕获异常的原因。

于 2016-07-20T11:16:41.513 回答