安全地将std::string_view转换为int(例如stoi或atoi)

37

是否有一种安全标准的方法将std::string_view转换为int


自从C++11出现后,我们可以使用stoistd::string转换为int

  std::string str = "12345";
  int i1 = stoi(str);              // Works, have i1 = 12345
  int i2 = stoi(str.substr(1,2));  // Works, have i2 = 23

  try {
    int i3 = stoi(std::string("abc"));
  } 
  catch(const std::exception& e) {
    std::cout << e.what() << std::endl;  // Correctly throws 'invalid stoi argument'
  }

但是 stoi 不支持 std::string_view。因此,我们可以使用 atoi,但是必须非常小心,例如:

  std::string_view sv = "12345";
  int i1 = atoi(sv.data());              // Works, have i1 = 12345
  int i2 = atoi(sv.substr(1,2).data());  // Works, but wrong, have i2 = 2345, not 23

因此atoi也不起作用,因为它基于空终止符'\0'(例如,sv.substr不能简单地插入/添加一个空终止符)。

现在,自C++17以来还有from_chars,但似乎在提供错误输入时不会引发异常:

  try {
    int i3;
    std::string_view sv = "abc";
    std::from_chars(sv.data(), sv.data() + sv.size(), i3);
  }
  catch (const std::exception& e) {
    std::cout << e.what() << std::endl;  // Does not get called
  }

2
这是因为 std::from_chars 不会抛出任何异常。相反,它会返回一个错误码。 - Yksisarvinen
1
当使用std::from_chars时,正如@Yksisarvinen所提到的,您需要检查返回的错误代码(result.rc)。此外,如果您想确保整个字符串都被转换为数字,还需要检查result.ptr == sv.data() + sv.size()。否则,对于像“12qq”这样的字符串,它将报告没有错误。 - Some Guy
3个回答

36

std::from_chars 函数不会抛出异常,它只返回类型为 from_chars_result 的结构体,其中包含两个字段:

struct from_chars_result {
    const char* ptr;
    std::errc ec;
};

当函数返回时,您应该检查ptrec的值:

#include <iostream>
#include <string>
#include <charconv>

int main()
{
    int i3;
    std::string_view sv = "abc";
    auto result = std::from_chars(sv.data(), sv.data() + sv.size(), i3);
    if (result.ec == std::errc::invalid_argument) {
        std::cout << "Could not convert.";
    }
}

1
如果您打算确保整个字符串已转换为数字,您还需要检查result.ptr == sv.data() + sv.size()(否则它将对类似“12qq”的字符串报告无错误),并且您还需要检查result.ec==errc{},而不是特定于std::errc::invalid_argument的错误,以捕获超出范围的错误等。 - Some Guy

8

很不幸,没有一种标准的方式可以为您抛出异常,但是std::from_chars具有可用的返回值代码:

#include <charconv>
#include <stdexcept>

template <class T, class... Args>
void from_chars_throws(const char* first, const char* last, T &t, Args... args) {
    std::from_chars_result res = std::from_chars(first, last, t, args... );

    // These two exceptions reflect the behavior of std::stoi.
    if (res.ec == std::errc::invalid_argument) {
        throw std::invalid_argument{"invalid_argument"};
    }
    else if (res.ec == std::errc::result_out_of_range) {
        throw std::out_of_range{"out_of_range"};
    }
}


显然,您可以从中创建svtoisvtol,但“扩展” from_chars的优点是您只需要一个模板函数。

如果您想确保整个字符串都被转换为数字,还需要检查result.ptr == last。否则,它将对像“12qq”这样的字符串报告无错误。 - Some Guy
@SomeGuy 这个答案的目的是模仿 std::stoi 的抛出行为。如果有剩余字符,std::stoi 不会抛出异常,因此这个版本也不会抛出异常。 - Holt
我很感激你的意见,但我仍然认为这是需要让任何使用它的人清楚理解的事情。根据我的经验,通常情况下将整个传入的字符串转换是非常有必要的,而不仅仅是其中的前几个字符。不幸的是,我遇到的C++开发人员中了解std::stoi会自动忽略尾随的无法解析字符的人还是比较少的,这通常会导致微妙的错误。 (另外,最近我被一个问题困扰了一段时间,即std::stoi会跳过前导空格,但std::from_chars却不会,所以你的代码仍然不能直接替换。) - Some Guy

6

在@Ron和@Holt的出色回答基础上,这里提供了一个小包装器,使用std::from_chars()并返回一个可选项(当输入无法解析时返回std::nullopt)。

#include <charconv>
#include <optional>
#include <string_view>

std::optional<int> to_int(const std::string_view & input)
{
    int out;
    const std::from_chars_result result = std::from_chars(input.data(), input.data() + input.size(), out);
    if(result.ec == std::errc::invalid_argument || result.ec == std::errc::result_out_of_range)
    {
        return std::nullopt;
    }
    return out;
}

ec 是除这两个之外的其他故障代码时怎么办? - M.M
@M.M,从CppReference文档中我所能看到的,这两个错误码是std::from_chars()唯一会返回的错误码。 - s3cur3
4
如果ec匹配std::errc {},则直接返回out,否则返回std::nullopt。这不是更简洁的选择吗?我认为std::string_view与迭代器相似,按值传递它们是正确的。 - 303
1
如果您想确保整个字符串都被转换为数字,还需要检查result.ptr == input.data()+input.size()。否则,它将对像“12qq”这样的字符串报告无错误。 - Some Guy

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