std::stoi是否真的安全可靠?

73
我曾与某人愉快地谈论过std::stoi的缺陷。坦率地说,它在内部使用std::strtol,并且如果std::strtol报告错误,则抛出异常。然而,根据他们的说法,std::strtol对于"abcxyz"这样的输入不应报告错误,这导致stoi不会抛出std::invalid_argument

首先,在GCC上测试了关于这些情况的行为的两个程序:
strtol
stoi

它们都显示"123"的成功和"abc"的失败。


我查阅了标准以获取更多信息:

§ 21.5

Throws: invalid_argument if strtol, strtoul, strtoll, or strtoull reports that  
no conversion could be performed. Throws out_of_range if the converted value is  
outside the range of representable values for the return type.

这概括了依赖strtol的行为。那么strtol呢?在C11草案中,我发现了以下内容:

§7.22.1.4

If the subject sequence is empty or does not have the expected form, no  
conversion is performed; the value of nptr is stored in the object  
pointed to by endptr, provided that endptr is not a null pointer.

在传递"abc"的情况下,C标准规定指向字符串开头的nptr将被存储在传递的指针endptr中。这似乎与测试结果一致。同样,应该返回0,如下所述:

§7.22.1.4

If no conversion could be performed, zero is returned.

之前的参考资料表示不会执行任何转换,因此必须返回0。这些条件现在符合C++11标准,使得 stoi 抛出 std::invalid_argument 异常。


对我来说,这个结果很重要,因为我不想去推荐使用 stoi 作为字符串转换成整数的更好选择,或者像它按照你所期望的那样工作一样使用它,如果它没有将文本捕获为无效转换。

经过所有这些后,我是否有什么地方做错了?在我的看来,我有这个异常被抛出的充分证据。我的证明是否有效,或者当给定 "abc" 时,std::stoi 是否保证会抛出该异常?


5
stoi的问题在于它可以成功地将 "123abc" 转为整数,这意味着它大多数时候都是无用的,除非你提供并检查结束索引参数。这使得它比 strtol 更难使用,因为你需要同时检查结束索引和捕获异常。 - interjay
2
@chris:我期望它会像boost::lexical_cast一样抛出异常。最常见的用例是当您想要转换整个字符串时,而stoi/l/ll需要大量样板代码来完成这项工作(或将其包装在函数中,也可以使用strtol完成)。 sto*的实现方式更加强大,但与strto*相比并没有提供任何优势。 - interjay
5
顺便提一下,“cin >> someInt”和“cin >> someString; someInt = stoi(someString);”不是相同的。如果输入为“123abc”,第一个版本将保留“abc”在流中,而第二个版本将默默地丢弃它。 - interjay
1
@interjay,是的,我明白你的观点,尤其如果这就是boost所做的。当我说“相同”时,我指的是整数的结果;我并没有考虑比较中的流。 - chris
1
James Kanze 是一个聪明的人,但聪明人也会犯错。你的错误是在截图中忘记了一个地方对他的名字进行审查。他的错误是忘记了即使 abcdef 使 strtol 返回 0,strtol 也必须报告返回 0 的原因是转换失败,所以 stoi 必须抛出其异常。但是,由于像 123abc 这样的情况,我仍然认为 stoi 是不安全的。 - Windows programmer
显示剩余9条评论
1个回答

89

std::stoi 在输入"abcxyz"时会抛出错误吗?

会。

我认为你的困惑可能来自于 strtol 从不报告除了溢出之外的错误。它可以报告没有执行转换,但在C标准中,这从未被称为错误条件。

strtol 被所有三个C标准以类似的方式定义,我会省略无聊的细节,但基本上它定义了一个“主体序列”,该序列是与实际数字相对应的输入字符串的子字符串。以下四个条件是等价的:

  • 主体序列具有预期形式(用简单的英语表述:它是一个数字)
  • 主体序列非空
  • 已进行转换
  • *endptr != nptr(仅在endptr非空时才有意义)

当溢出时,仍然认为已进行转换。

现在,很明显,因为"abcxyz"不包含数字,所以字符串"abcxyz"的主体序列必须为空,因此不能执行任何转换。以下的 C90/C99/C11 程序将通过实验证实:

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

int main() {
    char *nptr = "abcxyz", *endptr[1];
    strtol(nptr, endptr, 0);
    if (*endptr == nptr)
        printf("No conversion could be performed.\n");
    return 0;
}
这意味着任何符合标准的std::stoi实现,当没有提供可选基数参数时,给定输入"abcxyz"必须抛出invalid_argument异常。


这是否意味着std::stoi具有令人满意的错误检查功能?

不是的。当你使用std::strtol之后执行完整的检查errno == 0 && end != start && *end=='\0'时,你会发现她所说的话是正确的,因为std::stoi会默默地去除字符串中从第一个非数字字符开始的所有字符。

事实上,我能想到唯一一个其本地转换行为与std::stoi相似的语言是JavaScript,但即使如此,你也必须使用parseInt(n,10)强制使用十进制,以避免十六进制数字的特殊情况:

input      |  std::atoi       std::stoi      Javascript      full check 
===========+=============================================================
hello      |  0               error          error(NaN)      error      
0xygen     |  0               0              error(NaN)      error      
0x42       |  0               0              66              error      
42x0       |  42              42             42              error      
42         |  42              42             42              42         
-----------+-------------------------------------------------------------
languages  |  Perl, Ruby,     Javascript     Javascript      C#, Java,  
           |  PHP, C...       (base 10)                      Python...  

注意:不同编程语言在处理空格和多余的加号方面也有所不同。


好的,那么我想要完整的错误检查,我应该使用什么?

我不知道有没有内置函数可以做到这一点,但是boost::lexical_cast<int>可以满足你的需求。它特别严格,因为它甚至拒绝周围的空格,与Python的int()函数不同。请注意,无效字符和溢出会导致相同的异常,即boost::bad_lexical_cast

#include <boost/lexical_cast.hpp>

int main() {
    std::string s = "42";
    try {
        int n = boost::lexical_cast<int>(s);
        std::cout << "n = " << n << std::endl;
    } catch (boost::bad_lexical_cast) {
        std::cout << "conversion failed" << std::endl;
    }
}

是的,在Javascript中,== 在比较之前执行类型强制转换,以防有人在阅读此内容时感到困惑。 - Almo
1
为什么在C语言的表格中有std:atoi?我认为那应该是C++的。 - RedX
4
欢迎来到 C/C++ 标准库的奇妙世界:总是会有一小部分拼图缺失。 - Generic Human
4
你是否忘记了std::stoi有一个可选的第二个参数,可以报告不能被转换的输入字符串中第一个字符的索引位置?依我看,这意味着该函数确实执行了完整的错误检查。 - antred
感谢您提供详细、写得很好的答案!关于boost不接受空格的进一步评论,我们能否提到先修剪字符串是将数字解析为字符串的一个好方法?我认为boost的想法是让路,但确保他们的库中不存在意外行为。从某种意义上说,从“32 seymour street”中获取32可能是好的,因此如果您需要使用stoi,但如果您想基于字符串完全是数字运行一些逻辑,请使用像boost这样的库,并确保您提供了经过分别消毒的字符串。 - MrMesees
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接