C++11 引入了采用“const char*”的异常构造函数。但为什么?

C++11 introduced exception constructors taking `const char*`. But why?

Standard library defect #254 涵盖了新异常构造函数的添加:

std::logic_error::logic_error(const char* what_arg);
std::runtime_error::runtime_error(const char* what_arg);
// etc.

认为存储 std::strings 会打开一些与潜在内存分配问题相关的蠕虫罐头。

然而,following initiation of a discussion by orlp in the Lounge,令我震惊的是,除非标准强制要求 what_arg 只是一个字符串文字(或指向其他静态存储持续时间缓冲区的指针),否则它无论如何都必须执行 C 字符串的副本,以保持成员函数 what().

的明确定义

那是因为:

void bar() {
   char buf[] = "lol";
   throw std::runtime_error(buf);
}

void foo() {
   try {
      bar();
   }
   catch (std::exception& e) {
      std::cout << e.what() << '\n';   // e.what() points to destroyed data!
   }
}

但我看不到任何此类授权。事实上,异常对象是否深拷贝 what_arg 似乎完全未指定。

如果他们,那么一开始添加重载(消除额外分配)的大部分理由似乎完全是空洞的。

这可能是一个标准缺陷,还是我遗漏了什么?
这只是 "programmer: don't pass dangling pointers anywhere" 的一个例子吗?

我不确定这是否是原因,但一件事是 runtime_error 保证它的复制构造函数不会抛出,这表明某种引用计数机制。

额外的构造函数意味着它只需要制作一个副本,从 char* 到底层机制,而不是两次。一次进入字符串再进入引用机制

这允许(或者至少显然是为了促进——见下文)实现在它可以检测到(通过本身不是标准化的方式)传递的是字符串文字的情况下消除副本或其他具有静态存储持续时间的东西。

例如,假设编译器将所有字符串文字合并到一个由 __string_literals_begin__string_literals_end 分隔的范围内。然后在 std::exception 的构造函数内部的某处,它可以包含以下一般顺序的代码:

namespace std {
    exception::exception(char const *s) { 
        if (in_range(s, __string_literals_begin, __string_literals_end)) {
            stored_what = s;
            destroy_stored_what = false;
        }
        else {
            stored_what = dupe(s);
            destroy_stored_what = true;
        }
        // ...
    }

    exception::~exception() {
        if (destroy_stored_what)
            delete_string(stored_what);
}

链接的 DR 中的最后评论指出:

[ Oxford: The proposed resolution simply addresses the issue of constructing the exception objects with const char* and string literals without the need to explicit include or construct a std::string. ]

因此,根据当时的评论,委员会意识到这些超载并不能满足所有需求,但确实解决了(至少被认为是)需求。

实现 可以 提供这些重载,这(几乎)肯定是正确的,即使它们没有被标准强制要求——尽管如此,委员会似乎已经确信添加它们很有用,主要(如果不是唯一的话)用于上述情况 - 当字符串文字传递给 ctor 时仅进行浅表复制。

libc++ 是如何处理这个问题的?

他们目前使用引用计数字符串来存储消息:

class _LIBCPP_EXCEPTION_ABI logic_error
    : public exception
{
private:
    _VSTD::__libcpp_refstring __imp_;

其中__imp_初始化如下:

logic_error::logic_error(const string& msg) : __imp_(msg.c_str()) {}

logic_error::logic_error(const char* msg) : __imp_(msg) {}

这个字符串 __libcpp_refstring 在存储新的 char const*:

时确实分配了一个新的缓冲区
explicit __libcpp_refstring(const char* msg) {
        std::size_t len = strlen(msg);
        _Rep_base* rep =
             static_cast<_Rep_base *>(::operator new(sizeof(*rep) + len + 1));

但是,当然,__libcpp_refstring 的复制构造函数不会分配新的缓冲区。

(是的,这并没有回答问题,但我认为它应该可以阐明这个问题。例如,如果 logic_error 中只有一个 std::string const& ctor,那么必须为 ref-counter 增加一个分配。)

缺陷的解决方案解决了不同的问题。缺陷底部有这个注释:

[ Oxford: The proposed resolution simply addresses the issue of constructing the exception objects with const char* and string literals without the need to explicit include or construct a std::string. ]