【发布时间】: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,但不完全(相差几百万)。如果我在没有优化的情况下编译,那么pos 是18446744073703251929,所以它看起来像未初始化的垃圾,尽管我确实初始化了它(到 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。对我来说,这看起来像是实现中的错误。