c++ 11 std::stoi在base不在[2,36](GCC)时静默失败

C++11 std::stoi silently fails when base not in [2,36] (GCC)

本文关键字:GCC 失败 静默 stoi std base 不在 c++      更新时间:2023-10-16

我在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() vs atoi()的目标之一是更好的错误检测,但似乎根本没有检查base


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

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
  char* pos = (char*)42;
  printf("result: %ldn", 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

现在我们看到为什么pos在c++版本中是一个"垃圾"值:这是因为strtol()使endptr保持不变,而c++包装器错误地从中减去了输入字符串的起始地址。

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

[C++11: 21.5/3]:抛出:invalid_argument如果strtol, strtoul, strtoll,或strtoull报告不能进行转换。 [. .]

[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;只有当基类型有效,但输入值在该基类型中不可转换时,才会出现异常。