【问题标题】:Does std::exception own `what()`?std::exception 是否拥有`what()`?
【发布时间】:2016-07-20 10:58:07
【问题描述】:

我正在导出我自己的异常,将其命名为 MyException,来自 std::system_error 并覆盖 what() 以计算并返回我的消息。 MyException 的初始值设定项列表不会调用 system_error 构造函数覆盖接收消息。

如果我捕获 MyException 并将其复制到 std::exception,则在 std::exception 上调用 what() 的结果是 nullptr。这是有道理的。

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

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

我有点担心“what”字符串的所有权,因为what() 返回的是char*,而不是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;
    }

【问题讨论】:

  • 请出示您的代码。并且“如果我捕获了 MyException 并将其复制到 std::exception ” - 你已经分割了你的异常对象。 what 结果在被调用之前可能不存在。
  • @RichardCritten 添加了代码。我理解切片。
  • 然而,std::runtime_error 拥有它的字符串。具体来说,一个不可变的引用计数字符串,而不是 std::string
  • @ildjarn 这看起来像是一个答案。我猜可能是这种情况,但通过谷歌搜索找不到。如果它在 Visual Studio 中有效,则不想假设这就是标准的定义方式。

标签: c++ exception c++14 std std-system-error


【解决方案1】:

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

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

首先,std::exception 不拥有 whatstd::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:

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

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

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

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

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

不!当您制作MyExceptionstd::exception 副本 时,您将slicing 对象缩减为比物理存储what 值的位置更少派生的类型。如果您必须复制您的异常,您可以使用的最小派生类型是std::runtime_error。 (当然,您始终可以安全地将std::exceptionreference 设为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());
}

我会说编写分配的异常构造函数的形式很糟糕(出于显而易见的原因),但鉴于我所知道的每个当前标准库实现都使用short-string optimization 来代替std::basic_string&lt;&gt;,这极不可能在实践中永远是一个问题。

【讨论】:

    【解决方案2】:

    您的问题与了解异常的生命周期有关。这个问题在 herehere 的帖子中讨论过,可能会有所帮助。

    您可以保证使用智能指针延长异常的生命周期。我不确定性能影响是什么,但您可能会使用它来保留您自己的 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 here 上读到的使用异常指针的示例的重写,但我使用的是您的异常类,而不是像 std::logic_error 这样的标准异常。

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

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

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

    【讨论】:

      【解决方案3】:

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

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

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

      【讨论】:

        猜你喜欢
        • 2015-04-17
        • 2015-08-04
        • 1970-01-01
        • 2020-10-25
        • 2021-10-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多