【问题标题】:C++11 introduced exception constructors taking `const char*`. But why?C++11 引入了采用 `const char*` 的异常构造函数。但为什么?
【发布时间】:2015-05-17 03:21:48
【问题描述】:

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 只是一个字符串文字(或指向其他静态存储持续时间缓冲区的指针),否则它必须执行复制无论如何,为了保持成员函数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 似乎完全没有说明。

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

这可能是一个标准缺陷,还是我在这里遗漏了什么?
这只是“程序员:不要在任何地方传递悬空指针”的一个例子吗?

【问题讨论】:

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


    【解决方案1】:

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

    例如,假设编译器将所有字符串文字汇集到由__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:提议的解决方案只是解决了使用 const char* 和字符串文字构造异常对象的问题,而无需显式包含或构造 std::string。 ]

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

    (几乎)可以肯定的是,即使没有标准强制要求,实现可以提供这些重载——尽管如此,委员会似乎已经相信添加它们是有用的,主要是(如果不是唯一的)对于上面概述的情况——当一个字符串文字被传递给ctor时只做一个浅拷贝。

    【讨论】:

    • 是否允许实现为这样的优化添加构造函数,无论标准添加它们,只要行为不改变?
    • @orlp:是的,正如缺陷文本所说。
    • @LightnessRacesinOrbit 然后我会说 -1 并且不接受,因为这并不能解释为什么要进行此添加。
    • 博士不主张引用计数吗?
    • @JerryCoffin 我还是有点困惑。链接的缺陷似乎是实现想法的随机集合——并非全部有效,直到最终任意决定添加此构造函数。如果基本原理如您所说 - 对特定实施的非常强烈的建议,我希望缺陷中给出的基本原理是实施它的清晰简洁的建议。然而事实并非如此。
    【解决方案2】:

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

    附加的构造函数意味着它只需要复制一份,从 char* 到底层机制,而不是两次。一次进入字符串再进入引用机制

    【讨论】:

    • 如何复制到string(从string构造时)保证noexcept
    • @Walter 复制构造函数是noexcept,而不是构造函数。
    【解决方案3】:

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

    [ Oxford:提议的解决方案只是解决了使用 const char* 和字符串文字构造异常对象的问题,而无需显式包含或构造 std::string。 ]

    【讨论】:

      【解决方案4】:

      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&amp; ctor,则必须有一个额外的分配用于参考计数器。)

      【讨论】:

      • libstdc++ 要复杂得多;似乎他们有多个案例(预处理器开关),因为他们的旧 std::string 是 COW,但新的不是。
      • 据我所知,libstdc++ 和 libc++ 都没有执行@JerryCoffin 提到的优化。
      猜你喜欢
      • 2018-07-13
      • 2011-10-19
      • 2013-12-19
      • 1970-01-01
      • 2018-11-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多