C++: validating untrusted input with std::from_chars

Parse a port number strictly and reject anything that is not exactly a number in range.

C++
#include <charconv>
#include <iostream>
#include <optional>
#include <string_view>

std::optional<int> parse_port(std::string_view text) {
  int value = 0;
  const char* end = text.data() + text.size();
  auto [ptr, ec] = std::from_chars(text.data(), end, value);
  if (ec != std::errc() || ptr != end) {
    return std::nullopt;
  }
  if (value < 1 || value > 65535) {
    return std::nullopt;
  }
  return value;
}

int main() {
  for (std::string_view input : {"8080", "0", "70000", "80a", "", " 22"}) {
    auto port = parse_port(input);
    std::cout << "'" << input << "' -> ";
    if (port) {
      std::cout << *port << "\n";
    } else {
      std::cout << "rejected\n";
    }
  }
}
Output
'8080' -> 8080
'0' -> rejected
'70000' -> rejected
'80a' -> rejected
'' -> rejected
' 22' -> rejected

Explanation

All input from users, files, and networks is untrusted. Validate it before use: check that it has the right form, that the whole string was consumed, and that the value is in the allowed range. Reject what does not match, rather than trying to repair it.

std::from_chars parses a number from a character range without locale handling, exceptions, or whitespace skipping. It reports an error code and the position where parsing stopped. The function accepts the input only if parsing succeeded, consumed every character, and the value is in 1 to 65535. It returns std::optional<int> so the caller must handle the rejected case.

Explore the dry run
Guided walkthroughReady
1#include <charconv>
2#include <iostream>
3#include <optional>
4#include <string_view>
5
6std::optional<int> parse_port(std::string_view text) {
7 int value = 0;
8 const char* end = text.data() + text.size();
9 auto [ptr, ec] = std::from_chars(text.data(), end, value);
10 if (ec != std::errc() || ptr != end) {
11 return std::nullopt;
12 }
13 if (value < 1 || value > 65535) {
14 return std::nullopt;
15 }
16 return value;
17}
18
19int main() {
20 for (std::string_view input : {"8080", "0", "70000", "80a", "", " 22"}) {
21 auto port = parse_port(input);
22 std::cout << "'" << input << "' -> ";
23 if (port) {
24 std::cout << *port << "\n";
25 } else {
26 std::cout << "rejected\n";
27 }
28 }
29}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Parse an int; ptr marks where parsing stopped and ec reports failure.ec = errc() on success
—
Reject if parsing failed or if there are leftover characters, such as the a in 80a.ec = errc() on success
—
Check the allowed range after the form is valid.ec = errc() on success
—
Each input is tested; only 8080 survives all three checks.ec = errc() on success
'8080' -> 8080
'0' -> rejected
'70000' -> rejected
'80a' -> rejected
'' -> rejected
' 22' -> rejected

Cost and common mistakes

O(n) for an input of n characters.

  • atoi and stoi quietly accept trailing text and leading spaces, so '80a' would parse as 80.
  • Validating on the client side is not enough. Always validate on the side that trusts the value.
  • A value being in range does not make it safe for every use. Context, such as file paths or SQL, needs its own handling.

Try it yourself

How would you also allow surrounding spaces?

Reveal answer and reasoning

Trim the spaces first, then parse the trimmed view; do not make the parser itself lenient.

Further reading