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
- 1Convert a string to int with std::stoi or std::from_chars
- 2What stoi, from_chars, atoi and a stream do with bad input
- 3Reject trailing characters: check that the whole string was used
- 4Parse a std::string_view without copying
- 5Out of range: int, long, long long and unsigned
- 6String to double: std::stod, std::from_chars and the locale
- 7Why not atoi, sscanf or a stringstream
- 8C++23 and C++26: constexpr parsing and a result you can test
- 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.

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 errnostd::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:: when the text holds no number and std::out_of_range when the number does not fit. The running example reads a port number:
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:
"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:
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:
"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:: |
not written, failbit |
"abc" |
0 | throws invalid_argument |
errc:: |
0, failbit |
"42abc" |
42 | 42, stops at 2 | 42, stops at 2 | 42, stops at 2 |
" 42" |
42 | 42 | errc:: |
42 |
"+42" |
42 | 42 | errc:: |
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:: |
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,ptrand the stream’s position show that text was left over;atoihas 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.

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:
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:
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:
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:: 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:: |
errc:: |
std::from_chars into std::int64_t |
99999999999 | 99999999999 |
std::stoul("-1") |
4294967295 | 18446744073709551615 |
std::from_chars of "-1" into unsigned |
errc:: |
errc:: |
std::stoithrows on Linux too, where thelongthatstrtolreturns holds 11 digits: [string.conversions] requiresout_of_rangewhenever the value does not fit the return type, not only whenstrtolsetsERANGE.- After
result_out_of_rangethe value is untouched andptris past the last digit (11 of 13 characters for"99999999999px"on all three libraries): [charconv.from.chars] pointsptrat the first character that does not match the pattern, whether or not the value fits. - There is no
std::stou, andstd::stoul("-1")does not throw: the minus is applied in the unsigned type, which givesULONG_MAX, a different number on each platform. - For a number that needs 64 bits, call
std::stollor from_chars intostd::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:
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:
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:: 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
atoireturns 0 when there is no number, so"0"and"abc"look the same, and a number out ofintrange is undefined behavior. Microsoft documentsINT_MAXorINT_MINthere; the standard promises nothing.sscanfwith%dreturns how many values it assigned, which says a number was read but not that it fit: overflow is undefined behavior here too.istringstream >> nis correct if you test the stream: each failure in the table sets failbit, blank input also leavesnas 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:
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:
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):
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:
"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::), 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:: 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.
Sources
- std::stoi, std::stol, std::stollcppreference.com
- std::from_charscppreference.com
- [string.conversions] Numeric conversionsC++ standard working draft
- [charconv.from.chars] Primitive numeric input conversionC++ standard working draft
- std::atoi, std::atol, std::atollcppreference.com
- P2291R3: Add Constexpr Modifiers to Functions to_chars and from_chars for Integral Types in <charconv> HeaderWG21
- P2497R0: Testing for success or failure of <charconv> functionsWG21
- bugprone-unchecked-string-to-number-conversionclang-tidy documentation