【问题标题】:C++11 std::stoi silently fails when base not in [2,36] (GCC)当 base 不在 [2,36] (GCC) 中时,C++11 std::stoi 静默失败
【发布时间】:2014-08-21 15:49:40
【问题描述】:

我在 Linux 上使用 GCC 4.9.0。这是我的测试程序:

#include <iostream>
#include <string>

using namespace std;

int main(int argc, char* argv[])
{
  size_t pos = 42;
  cout << "result: " << stoi(argv[1], &pos, atoi(argv[2])) << '\n';
  cout << "consumed: " << pos << '\n';
}

这是一个预期的结果:

$ ./a.out 100 2
result: 4
consumed: 3

也就是说,它将以 2 为底的“100”解析为数字 4,并消耗了所有 3 个字符。

我们可以在 36 以内进行类似操作:

 $ ./a.out 100 36
result: 1296
consumed: 3

但是更大的基地呢?

$ ./a.out 100 37
result: 0
consumed: 18446744073707449552

这是什么? pos 应该是它停止解析的索引。这里接近std::string::npos,但不完全(相差几百万)。如果我在没有优化的情况下编译,那么pos18446744073703251929,所以它看起来像未初始化的垃圾,尽管我确实初始化了它(到 42)。事实上,valgrind 抱怨道:

Conditional jump or move depends on uninitialised value(s)
  at 0x400F11: int __gnu_cxx::__stoa<long, int, char, int>(...) (in a.out)
  by 0x400EC7: std::stoi(std::string const&, unsigned long*, int) (in a.out)

所以这很有趣。此外,std::stoi 的文档说,如果无法执行转换,它会抛出 std::invalid_argument。显然在这种情况下它没有进行任何转换,它在pos 中返回了垃圾,并且没有抛出异常。

如果base 为 1 或负数,也会发生类似的坏事。

这是 GCC 实现中的错误,标准中的错误,还是我们必须学会忍受的东西?我认为stoi()atoi() 的目标之一是更好的错误检测,但似乎根本不检查base


编辑:这是同一程序的 C 版本,它也打印 errno:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
  char* pos = (char*)42;
  printf("result: %ld\n", strtol(argv[1], &pos, atoi(argv[2])));
  printf("consumed: %lu (%p)\n", pos - argv[1], pos);
  perror("errno");
  return 0;
}

当它工作时,它会做和以前一样的事情。当它失败时,它会更加清晰:

$ ./a.out 100 37
result: 0
consumed: 18446603340345143502 (0x2a)
errno: Invalid argument

现在我们明白了为什么 C++ 版本中的 pos 是一个“垃圾”值:这是因为 strtol() 保持 endptr 不变,并且 C++ 包装器错误地从中减去了输入字符串的起始地址。

在 C 版本中我们还看到 errno 设置为 EINVAL 以指示错误。我系统上的文档说当base 无效时会发生这种情况,但也说它不是由C99 指定的。如果我们在 C++ 版本中打印 errno,我们也可以检测到这个错误(但它在 C99 中不是标准的,并且肯定不是 C++11 指定的)。

【问题讨论】:

  • 来自 cpp ref:异常:如果无法执行转换,则会引发 invalid_argument 异常。如果读取的值超出 int 可表示值的范围,则会引发 out_of_range 异常。无效的 idx 会导致未定义的行为。我不认为他们对大于 36 的基数实现了更好的错误处理,仅仅是因为没有足够的 ASCII 符号来使用大于 36 的基数。
  • @Unda 我同意默默失败的事情令人担忧......
  • 请注意,C++11 定义了 stoi 在调用 strtol 时应该做什么。反过来,C99 标准是否 定义当 base 不是 0 或介于 2 和 36 之间时会发生什么(有些实现设置了 EINVAL,有些没有)。无论如何,__stoa(由std::stoi 调用,传递std::strtol)也不检查EINVAL:gcc.gnu.org/onlinedocs/gcc-4.8.1/libstdc++/api/…。您可以尝试使用 C 纯测试用例吗?我想你已经发现了一个错误。
  • 目前尚不清楚您正在寻找什么补救措施。假定 C 语言没有 E_PEBKAC。
  • @Unda:我现在检查了 GCC 4.9.0 的源代码。其中__stoa() 设置errno = 0 并通过函数指针调用strtol()。然后它检查endptr == str 是否已解析任何内容,但如果base 无效,则endptr 不会被strtol() 更改。它本身从未初始化endptr,所以它是垃圾,因此__stoa() 与垃圾进行比较,结果不确定(但可能测试失败,所以它不会抛出)。最后,它检查不适用的errno == ERANGE,然后错误地分配给pos。对我来说,这看起来像是实现中的错误。

标签: c++ gcc c++11 std


【解决方案1】:

[C++11: 21.5/3]: 抛出:invalid_argument 如果strtolstrtoulstrtollstrtoull 报告无法执行转换。 [..]

[C99: 7.20.1.4/5]: 如果主题序列具有预期的形式并且base 的值为零,则根据 6.4.4.1 的规则,从第一个数字开始的字符序列被解释为整数常量。如果主题序列具有预期的形式,并且base 的值介于 2 和 36 之间,则将其用作转换的基础,并为每个字母赋予上面给出的值。 [..]

对于base 0 或介于2 到36 之间的情况,C99 中没有指定语义,因此结果未定义。这不一定满足[C++11: 21.5/3]的摘录。

简而言之,这是 UB;只有当基数有效但输入值在该基数中不可转换时,您才会期望出现异常。 这既不是 GCC 也不是标准中的错误。

【讨论】:

  • 嗯,对我来说,这听起来像是 C 标准中的一个错误:它应该准确地指定当 base 无效时会发生什么。不指定它是很懒惰的。 (是的,我确实理解目前存在大量不兼容的实现,部分原因正是因为标准从不关心澄清这一点。)
  • 你可能是对的,但不幸的是,当基数无效时,GCC 会产生未定义的行为,而产生一个直观的、实现定义的抛出行为是如此微不足道。只需让他们的__stoa()endptr 初始化为输入字符串,然后在这种情况下神奇地抛出invalid_argument。这似乎是一种情况,C 没有指定当base 无效时会发生什么,然后通用实现(GCC,Clang)指定了它(EINVAL,正如人们所期望的那样)。然后 C++ 再次未指定它,但这次 GCC 离开了它。
  • @peppe:如果它不“打扰”为前置条件失败指定语义,您是否实质上是在说明该标准存在缺陷?对不起,不行。世界不是这样运作的。它甚至不应该是您自己的代码的工作方式!!
  • 嗯,好吧,就是说,在这种特殊情况下,我可能期望更好。
  • 来吧,它无法与operator[] 相提并论。它不是内联的。这不是 O(1)。实际运行的代码很难估计。它有一个循环和一些整数数学,包括除法和模数。没有标准化这种基本检查的结果(特别是 glibc 和 BSD 的 libc 已经完成了,对于base == 0 的情况,无论如何你都需要这样做),如果不是懒惰,它也非常接近于懒惰。
猜你喜欢
  • 2012-11-15
  • 2019-06-09
  • 2017-02-25
  • 2014-02-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多