Standard Library[lib]

C++ string to int: std::stoi or std::from_chars, not atoi

Section
Standard Library [lib]
Standard
C++17
Compilers
MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2, Clang 23.1 (libc++)
Published
Length
11 min read
In this answer9 clauses
  1. 1Convert a string to int with std::stoi or std::from_chars
  2. 2What stoi, from_chars, atoi and a stream do with bad input
  3. 3Reject trailing characters: check that the whole string was used
  4. 4Parse a std::string_view without copying
  5. 5Out of range: int, long, long long and unsigned
  6. 6String to double: std::stod, std::from_chars and the locale
  7. 7Why not atoi, sscanf or a stringstream
  8. 8C++23 and C++26: constexpr parsing and a result you can test
  9. 9Which one to call

Abstract

Call std::stoi (C++11) on a std::string when bad input should throw, and std::from_chars (C++17) on a std::string_view or a buffer when it should not: it returns an error code, ignores the locale and does not allocate. Both stop at the first character that is not part of the number, so also check pos == s.size() or ptr == last. Avoid atoi: it returns 0 for “abc” and its overflow is undefined behavior.

A row of five square cells, the first three outlined in black and the last two in grey. A bracket under the black cells leads a blue arrow down to one wide hatched cell, four cells long.
Figure 1 — A parse reads characters from the front and stops; what it did not read is still there.

To convert a C++ string to int, call std::stoi(s) (C++11, <string>) when the text is a std::string and bad input should throw. Call std::from_chars (C++17, <charconv>) for a std::string_view or a buffer, for bad input that is routine, or for a result that must not depend on the locale: it returns an error code and allocates nothing. Either way, check that the whole string was used: both read "42abc" as 42 and report success. Do not call atoi: it returns 0 for "abc" and for "0", and a number too large for int is undefined behavior.

This answer is written for C++17, which added std::from_chars. std::stoi is older (C++11); C++23 makes the integer from_chars constexpr, and C++26 lets you test its result as a bool. The samples were built with four compilers on three standard libraries: MSVC 19.44 and Clang 19.1 on Microsoft’s STL (Windows x64), GCC 16.2 with libstdc++ and Clang 23.1 with libc++ (Compiler Explorer, Linux x86-64). Outputs are what all four printed unless the text says otherwise; Since When’s C++17 table gives the releases that complete <charconv>.

What changed: text becomes an int through atoi, or strtol and a check of errno std::from_chars, or std::stoi where an exception suits.

since C++17

Convert a string to int with std::stoi or std::from_chars

std::stoi returns the int, and throws std::invalid_argument when the text holds no number and std::out_of_range when the number does not fit. The running example reads a port number:

[Example 1:
try {
    int port = std::stoi(text);
    std::cout << port << '\n';
} catch (const std::invalid_argument& e) {   // no number
    std::cout << "invalid_argument: " << e.what() << '\n';
} catch (const std::out_of_range& e) {   // too large for int
    std::cout << "out_of_range: " << e.what() << '\n';
}

— end example]

With each input printed before it, Microsoft’s STL gave:

[Example 2:
"8080": 8080
"80a": 80
"": invalid_argument: invalid stoi argument
" 80": 80
"99999999999": out_of_range: stoi argument out of range

— end example]

The what() text is each library’s own (libstdc++ prints stoi for both, libc++ stoi: no conversion and stoi: out of range), so catch the type, not the message.

std::from_chars takes the text as two pointers and returns where it stopped (ptr) and an error code (ec); it throws nothing. Checking both gives the parse the rest of this page uses:

[Example 3:
std::optional<int> parse_int(std::string_view text) {
    int value = 0;
    const char* first = text.data();
    const char* last = first + text.size();
    auto [ptr, ec] = std::from_chars(first, last, value);
    if (ec != std::errc{} || ptr != last) return std::nullopt;
    return value;
}
// ...
std::string line = "8443";
std::optional<int> port = parse_int(line);   // converts to a view

— end example]

All four printed:

[Example 4:
"8080": 8080
"80a": rejected
"": rejected
" 80": rejected
"99999999999": rejected
std::string "8443": 8443

— end example]

A parse that can fail returns a std::optional, the type for a result that may be missing, and the view goes by value, the rule for a std::string_view parameter. What each call gives up: std::stoi needs a std::string and throws; std::from_chars wants two pointers and skips no whitespace.

What stoi, from_chars, atoi and a stream do with bad input

One program sent ten inputs through the four ways of parsing. A cell that reads “stops at n” is a success that used only the first n characters. All three libraries printed the same table:

Input atoi std::stoi std::from_chars istringstream >> int
"42" 42 42 42 42
"" 0 throws invalid_argument errc::invalid_argument not written, failbit
"abc" 0 throws invalid_argument errc::invalid_argument 0, failbit
"42abc" 42 42, stops at 2 42, stops at 2 42, stops at 2
" 42" 42 42 errc::invalid_argument 42
"+42" 42 42 errc::invalid_argument 42
"-42" -42 -42 -42 -42
"0x1F" 0 0, stops at 1 0, stops at 1 0, stops at 1
"3.9" 3 3, stops at 1 3, stops at 1 3, stops at 1
"99999999999" undefined behavior (not run) throws out_of_range errc::result_out_of_range 2147483647, failbit
  • In base 10 none of them reads a hex prefix or a decimal point, and none calls that an error: "0x1F" is 0 and "3.9" is 3.
  • "42abc" is a success for all four. pos, ptr and the stream’s position show that text was left over; atoi has no way to say so.
  • " 42" and "+42" fail only in from_chars, which skips no whitespace and accepts a minus sign but no plus ([charconv.from.chars]).

Reject trailing characters: check that the whole string was used

"80a" read as 80 is the bug that ships. With std::stoi, pass &pos and compare it with text.size(); with std::from_chars, compare ptr with last.

The text 42abc in five cells: 4 and 2 beneath a bracket reading parsed: 42, then a, b and c in grey. Arrows point up at first, at ptr between 2 and a, and at last past the end.
Figure 2 — std::from_chars stops at the first character that is not part of the number; the whole string was a number only if ptr equals last.
[Example 5:
std::optional<int> parse_with_stoi(const std::string& text) {
    std::size_t pos = 0;
    int value = 0;
    try {
        value = std::stoi(text, &pos);
    } catch (const std::logic_error&) {   // both stoi exceptions
        return std::nullopt;
    }
    if (pos != text.size()) return std::nullopt;   // trailing text
    return value;
}

std::optional<int> parse_with_from_chars(std::string_view text) {
    int value = 0;
    const char* first = text.data();
    const char* last = first + text.size();
    auto [ptr, ec] = std::from_chars(first, last, value);
    if (ec != std::errc{}) return std::nullopt;     // no number
    if (ptr != last) return std::nullopt;           // trailing text
    return value;
}

— end example]

All four printed:

[Example 6:
input   stoi      from_chars
"8080"  8080      8080
"80a"   rejected  rejected
" 80"   80        rejected
"+80"   80        rejected
"80 "   rejected  rejected

— end example]

Only from_chars rejects " 80" and "+80", and neither accepts "80 ": for text that must match a format exactly, from_chars is the stricter check.

Parse a std::string_view without copying

std::stoi has no std::string_view overload: it takes a const std::string& or a const std::wstring& ([string.conversions]), so a view is copied into a string first. std::from_chars reads the characters where they are. A replaced global operator new counted the allocations for a 4-character field and for the 40-character config line it came from:

[Example 7:
int value = std::stoi(std::string(text));   // copies the text
// ...
const char* first = text.data();   // reads the text in place
auto [ptr, ec] = std::from_chars(first, first + text.size(), value);

— end example]

MSVC, Clang on Microsoft’s STL, and GCC printed:

[Example 8:
the field (4 characters)
  stoi(std::string(sv)): 8080, 0 allocation(s)
  from_chars:            8080, 0 allocation(s)
the line (40 characters)
  stoi(std::string(sv)): 8080, 1 allocation(s)
  from_chars:            8080, 0 allocation(s)
std::string's own buffer holds 15 characters

— end example]

libc++ printed the same counts and a 22-character buffer. An int written without leading zeros or spaces is at most 11 characters ("-2147483648"), shorter than the buffer on all three, so copying it does not allocate. The case for from_chars on a view is that nothing is copied and nothing throws.

Out of range: int, long, long long and unsigned

Too large for int, std::stoi throws std::out_of_range, and std::from_chars returns std::errc::result_out_of_range and leaves the value alone. The long versions depend on the platform: sizeof(long) printed 4 on Windows and 8 on Linux. Every row but the "-1" ones parses the 11 digits "99999999999":

Call Windows x64 (Microsoft’s STL) Linux x86-64 (libstdc++, libc++)
std::stoi throws out_of_range throws out_of_range
std::stol throws out_of_range 99999999999
std::stoll 99999999999 99999999999
std::from_chars into int errc::result_out_of_range errc::result_out_of_range
std::from_chars into std::int64_t 99999999999 99999999999
std::stoul("-1") 4294967295 18446744073709551615
std::from_chars of "-1" into unsigned errc::invalid_argument errc::invalid_argument
  • std::stoi throws on Linux too, where the long that strtol returns holds 11 digits: [string.conversions] requires out_of_range whenever the value does not fit the return type, not only when strtol sets ERANGE.
  • After result_out_of_range the value is untouched and ptr is past the last digit (11 of 13 characters for "99999999999px" on all three libraries): [charconv.from.chars] points ptr at the first character that does not match the pattern, whether or not the value fits.
  • There is no std::stou, and std::stoul("-1") does not throw: the minus is applied in the unsigned type, which gives ULONG_MAX, a different number on each platform.
  • For a number that needs 64 bits, call std::stoll or from_chars into std::int64_t.

String to double: std::stod, std::from_chars and the locale

std::stod (C++11) reads the decimal point of the C library’s current locale (LC_NUMERIC, set by std::setlocale); std::from_chars into a double reads . whatever the locale says. A program parsed "3.14" and "3,14" with both, switched LC_NUMERIC to a German locale with std::setlocale (de_DE.UTF-8 was accepted on all four builds, Windows included), and parsed again:

[Example 9:
std::size_t pos = 0;
double a = std::stod(text, &pos);
double b = 0;
const char* first = text.data();
auto [ptr, ec] =
    std::from_chars(first, first + text.size(), b);

— end example]

All four printed:

[Example 10:
the C locale, decimal point '.':
  "3.14"  stod: 3.14 (used 4)  from_chars: 3.14 (used 4)
  "3,14"  stod: 3 (used 1)  from_chars: 3 (used 1)
after setlocale(LC_NUMERIC, "de_DE.UTF-8"), decimal point ',':
  "3.14"  stod: 3 (used 1)  from_chars: 3.14 (used 4)
  "3,14"  stod: 3.14 (used 4)  from_chars: 3 (used 1)

— end example]

After the switch, std::stod("3.14") is 3, with no exception. from_chars did not change. Its floating-point overloads arrived after the integer ones in all three libraries, years later in libstdc++ and libc++:

Library float and double long double
Microsoft’s STL VS 2017 15.8 (integers since 15.7) built and ran
libstdc++ GCC 11.1 (integers since 8.1) built and ran
libc++ LLVM 20 (integers since 7) rejected: call to deleted function

For hexadecimal floats, from_chars wants std::chars_format::hex and no 0x: "1.8p3" gave 12 that way, while the default format read "0x1.8p3" as 0 and std::stod read it as 12.

Why not atoi, sscanf or a stringstream

  • atoi returns 0 when there is no number, so "0" and "abc" look the same, and a number out of int range is undefined behavior. Microsoft documents INT_MAX or INT_MIN there; the standard promises nothing.
  • sscanf with %d returns how many values it assigned, which says a number was read but not that it fit: overflow is undefined behavior here too.
  • istringstream >> n is correct if you test the stream: each failure in the table sets failbit, blank input also leaves n as it was, and an overflow stores the nearest limit (2147483647 or -2147483648). Trailing text is not an error.

clang-tidy flags the first two with bugprone-unchecked-string-to-number-conversion. The clang-tidy 19.1.5 bundled with Visual Studio predates that name and knows the check only as cert-err34-c, which is now an alias of it, so cert-err34-c is the name to put in a .clang-tidy file that both must read. Run on this file:

[Example 11:
int port_from_atoi(const std::string& text) {
    return std::atoi(text.c_str());   // 0 for "abc", and for "0"
}

int port_from_sscanf(const std::string& text) {
    int port = 0;
    std::sscanf(text.c_str(), "%d", &port);   // result not checked
    return port;
}

int port_from_sscanf_checked(const std::string& text) {
    int port = 0;
    if (std::sscanf(text.c_str(), "%d", &port) != 1) return -1;
    return port;   // still undefined if the number is too large
}

— end example]

clang-tidy trunk and 19.1.5 each warned once per call, the checked sscanf included: “‘atoi’ used to convert a string to an integer value, but function will not report conversion errors; consider using ‘strtol’ instead”, and the same with 'sscanf'. The advice is sound as far as it goes: strtol can report every failure, if you clear errno, check the end pointer and check the int range yourself, which is what from_chars does in one call.

MSVC (C4996) and Clang on Windows (-Wdeprecated-declarations) also warn about sscanf itself (“This function or variable may be unsafe”); the GCC and libc++ builds say nothing, and none of the four warns about atoi. clang-tidy and the other analyzers are on Toolchain, under analyzers.

C++23 and C++26: constexpr parsing and a result you can test

C++23 makes the integer overloads of from_chars constexpr (P2291R3), so parse_int can be checked at compile time:

[Example 12:
constexpr std::optional<int> parse_int(std::string_view text) {
    // ...
    auto [ptr, ec] =
        std::from_chars(first, last, value);   // C++23: constexpr
    // ...
}

static_assert(parse_int("8080") == 8080);
static_assert(!parse_int("80a"));
static_assert(!parse_int("99999999999"));

— end example]

All four compile it under C++23 and reject it under C++20, where Clang notes non-constexpr function 'from_chars' cannot be used in a constant expression. The libraries added it in libstdc++ 13.1, libc++ 16 and Visual Studio 2022 17.4.

C++26 gives from_chars_result an explicit conversion to bool, true when ec is errc{} (P2497R0):

[Example 13:
if (std::from_chars(first, last, value))   // C++26: bool
    std::cout << '"' << text << "\": " << value << '\n';
else
    std::cout << '"' << text << "\": no number\n";

— end example]

Under -std=c++26, GCC 16.2 and Clang 23.1 with libc++ printed:

[Example 14:
"8080": 8080
"80a": 80
"abc": no number

— end example]

The test reads the error code only: "80a" is true and gives 80, so comparing ptr with last is still yours to do. Microsoft’s STL rejects it under /std:c++latest (error C2451: a conditional expression of type 'std::from_chars_result' is not valid), and all three libraries reject it under C++23.

Which one to call

Situation Call Since
Text in a std::string, bad input is exceptional std::stoi, then pos == text.size() C++11
A std::string_view or a buffer, or bad input is routine std::from_chars, then ptr == last C++17
The result must not depend on the locale std::from_chars C++17
In a constant expression std::from_chars C++23
A long long or a fixed-width type std::stoll, or from_chars into std::int64_t C++11, C++17
A double std::stod, or from_chars where the library has it C++11, C++17
Legacy code with atoi or sscanf one of the above C++11, C++17

The reverse, a number to text, is in C++ int to string: to_string, format or to_chars. More answers about the library’s types and functions are in Standard library.

Questions

How do I check whether a string is a number in C++?

Parse it and require two things: no error, and every character used. With std::from_chars that is ec == std::errc{} and ptr equal to the end of the text; with std::stoi, no exception and pos equal to size(), though stoi also accepts leading spaces and a plus sign, which from_chars rejects. The standard library has no separate is-number function.

How do I convert a hex string to int in C++?

Pass the base. std::stoi(text, nullptr, 16) read both “1F” and “0x1F” as 31, and base 0 takes the base from the prefix. std::from_chars(first, last, value, 16) read “1F” as 31 but does not know the prefix: given “0x1F” it returned 0 and stopped after one character, so strip the prefix first.

What is the difference between atoi and std::stoi?

std::stoi (C++11) throws std::invalid_argument when the text holds no number and std::out_of_range when the number does not fit, and its pos argument reports how many characters it used. atoi returns 0 when there is no number, cannot say where it stopped, and is undefined behavior when the number is out of range. Both skip leading whitespace and ignore trailing text.

How do I convert a char to an int in C++?

For a digit character, subtract '0': '7' - '0' is 7, because the digit characters are contiguous. A cast gives the character code instead: static_cast<int>('7') printed 55 on all four compilers. Accept only '0' to '9' before subtracting.

Does std::from_chars work with a std::string?

Yes. Pass s.data() and s.data() + s.size(), or pass the string to a function that takes a std::string_view, which converts without copying the characters. from_chars reads any range of char, whoever owns it.

Read next

  1. std::optional in C++: how to use it and when not toStandard Library · Miriam Castellanos · written for C++17
  2. C++ map: std::map find, insert, erase and iterateStandard Library · Miriam Castellanos · written for C++17
  3. C++ int to string: to_string, format or to_chars, not itoaStandard Library · Miriam Castellanos · written for C++17