【问题标题】:Construct standard exceptions with null pointer argument and impossible postconditions使用空指针参数和不可能的后置条件构造标准异常
【发布时间】:2020-07-10 10:51:52
【问题描述】:

考虑以下程序:

#include<stdexcept>
#include<iostream>

int main() {
    try {
        throw std::range_error(nullptr);
    } catch(const std::range_error&) {
        std::cout << "Caught!\n";
    }
}

带有 libstdc++ 的 GCC 和 Clang 调用 std::terminate 并使用消息中止程序

terminate called after throwing an instance of 'std::logic_error'
  what():  basic_string::_S_construct null not valid

在构造异常时使用 libc++ 段错误。

godbolt

编译器的行为是否符合标准?标准[diagnostics.range.error] (C++17 N4659) 的相关部分确实说std::range_error 有一个const char* 构造函数重载,应该优先于const std::string&amp; 重载。该部分也没有说明构造函数的任何前置条件,只说明后置条件

后置条件strcmp(what(), what_­arg) == 0.

如果what_arg 是空指针,则此后置条件始终具有未定义的行为,这是否意味着我的程序也具有未定义的行为并且两个编译器的行为一致?如果不是,应该如何阅读标准中这些不可能的后置条件?


再想一想,我认为这对我的程序来说一定意味着未定义的行为,因为如果不是这样,那么(有效的)不指向空终止字符串的指针也将被允许,这显然没有意义。

因此,假设这是真的,我想将问题更多地集中在标准如何暗示这种未定义的行为上。是因为后置条件的不可能性,调用也有未定义的行为,还是前置条件被遗忘了?


灵感来自this question

【问题讨论】:

  • 听起来std::range_error 被允许通过引用存储东西,所以我不会感到惊讶。在传递nullptr 时调用what() 可能会导致问题。
  • @Chipster 我不确定你到底是什么意思,但在再次考虑这个问题后,我认为这一定是未定义的行为。我已经编辑了我的问题,以便更多地关注如何标准措辞暗示未定义的行为。
  • 如果 nullptr 被传递,我认为 what() 必须在某个时候取消引用它才能获得值。那将取消对nullptr 的引用,这充其量是有问题的,并且肯定会崩溃是最坏的。
  • 不过我同意。它必须是未定义的行为。但是,将其纳入解释原因的充分答案超出了我的技能范围。
  • 我认为这是一个前提条件,即参数指向一个有效的C字符串,因为strcmp用于描述what_arg的值。无论如何,这就是相关的section from the C standard 所说的,由&lt;cstring&gt; 的规范引用。当然,措辞可以更清楚。

标签: c++ exception language-lawyer


【解决方案1】:

来自doc

因为复制 std::range_error 是不允许抛出异常的, 此消息通常在内部存储为单独分配的 引用计数的字符串。这也是没有构造函数的原因 采用 std::string&&: 无论如何它都必须复制内容。

这说明了为什么会出现段错误,api 确实将其视为真正的字符串。 一般来说,如果某些东西是可选的,那么在 cpp 中,将会有一个重载的构造函数/函数,它不会采用它不需要的东西。因此,将nullptr 传递给未记录可选内容的函数将是未定义的行为。通常 API 不接受指针,但 C 字符串除外。因此,恕我直言,可以安全地假设为期望 const char * 的函数传递 nullptr 将是未定义的行为。对于这些情况,较新的 API 可能更喜欢 std::string_view

更新:

通常假设 C++ API 接受一个指针来接受 NULL 是公平的。但是 C 字符串是一种特殊情况。在std::string_view 之前,没有更好的方法可以有效地通过它们。一般来说,对于接受const char * 的 API,假设应该是它必须是一个有效的 C 字符串。即指向以“\0”结尾的chars 序列的指针。

range_error 可以验证指针不是nullptr,但它无法验证它是否以“\0”结尾。所以最好不要做任何验证。

我不知道标准中的确切措辞,但这个先决条件可能是自动假定的。

【讨论】:

    【解决方案2】:

    是的,这是未定义的行为。 [res.on.arguments]says:

    1.1 - 如果函数的参数具有无效值(例如 [...] 指针对其预期用途无效),则行为未定义。

    明确检查后置条件是函数(或构造函数)参数的预期用途。

    空指针在这里作为strcmp 的参数是否无效? [cstring.syn] refers us 到 C 库;参考相应的标准(specifically C17 根据 [intro.refs],并不是说我们参考哪个 C 标准版本会有所不同),在 7.1.4 库函数的使用中我们阅读:

    1 - [...] 如果函数的参数具有无效值(例如 [...] 空指针 [...])[...] 行为未定义。

    然后,7.24.1 字符串函数约定说:

    1 - 标题&lt;string.h&gt; 声明[...] 几个函数[...] 可用于操作字符类型数组和其他被视为字符类型数组的对象。 [...] 在所有情况下,char *void * 参数都指向数组的初始(最低地址)字符。

    7.24.4.2 strcmp函数说:

    2 - strcmp 函数将s1 指向的字符串与s2 指向的字符串进行比较。

    然后,回到7.1.1 术语定义,我们读到:

    1 - string 是一个连续的字符序列,由第一个空字符终止并包括第一个空字符。 [...] 指向 字符串 是指向其初始(最低地址)字符的指针。

    所以空指针是strcmp 的无效参数,因此检查后置条件具有未定义的行为,因此空指针作为std::range_error 的构造函数参数的预期用途是无效的,因此程序肯定具有未定义的行为;两个编译器都是一致的,因为在运行时遇到未定义的行为时,任何事情都可能发生。

    【讨论】:

      【解决方案3】:

      这又回到了一个基本问题,从 nullptr 创建一个 std::string 可以吗?它应该做什么?

      www.cplusplus.com

      如果 s 是空指针,如果 n == npos,或者如果 [first,last) 指定的范围无效,则会导致未定义的行为。

      所以当

      throw std::range_error(nullptr);
      

      被称为实现试图做出类似的东西

      store = std::make_shared<std::string>(nullptr);
      

      这是未定义的。我认为这是一个错误(没有阅读标准中的实际措辞)。相反,libery 开发人员可以做出类似的东西

      if (what_arg)
        store = std::make_shared<std::string>(nullptr);
      

      但随后捕手必须检查 what(); 中的 nullptr ,否则它将在那里崩溃。所以std::range_error 应该像其他一些语言一样分配一个空字符串或“(nullptr)”。

      【讨论】:

      • “这又回到了一个基本问题,从 nullptr 创建一个 std::string 可以吗?” ——我不这么认为。
      • 我认为标准没有指定异常必须保存std::string 的任何地方,并且重载决议不应选择std::string 构造函数。
      • const char * 重载由重载决议选择
      猜你喜欢
      • 2016-10-13
      • 2016-06-02
      • 2015-08-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多