scanf for modern C++
#include
#include // for std::println (C++23)
int main() {
// Read two integers from stdin
// with an accompanying message
if (auto result =
scn::prompt("What are your two favorite numbers? ", "{} {}")) {
auto [a, b] = result->values();
std::println("Oh, cool, {} and {}!", a, b);
} else {
std::println(stderr, "Error: {}", result.error().msg());
}
}Try out in Compiler Explorer.
scnlib is a modern C++ library for replacing scanf and std::istream.
This library attempts to move us ever so much closer to replacing iostreams
and C stdio altogether.
It's faster than iostream (see Benchmarks), and type-safe, unlike scanf.
Think {fmt} or C++20 std::format, but in the
other direction.
This library is the reference implementation of the ISO C++ standards proposal P1729 "Text Parsing".
The documentation can be found online, at https://scnlib.dev.
To build the docs yourself, build the scn_docs target generated by CMake.
These targets are generated only if the variable SCN_DOCS is set in CMake
(done automatically if scnlib is the root project).
The scn_docs target requires Doxygen, Python 3.8 or better, and the pip3
package poxy.
See more examples in the examples/ folder.
std::string…#include
int main() {
auto input = std::string{"123 456 foo"};
auto result = scn::scan(input, "{} {}");
// result == true
// result->range(): " foo"
// All read values can be accessed through a tuple with result->values()
auto [a, b] = result->values();
// Read from the remaining input
// Could also use scn::ranges::subrange{result->begin(), result->end()} as input
auto result2 = scn::scan(result->range(), "{}");
// result2 == true
// result2->range().empty() == true
// result2->value() == "foo"
}#include
#include
int main() {
auto result = scn::scan("123" | std::views::reverse, "{}");
// result == true
// result->begin() is an iterator into a reverse_view
// result->range() is empty
// result->value() == 321
}#include
#include
int main() {
std::vector vec{};
auto input = scn::ranges::subrange{"123 456 789"sv};
while (auto result = scn::scan(input), "{}")) {
vec.push_back(result->value());
input = result->range();
}
}scn::scan, no output parameters)scn::module with SCN_MODULES in CMake)"{python}"-like format string syntaxscnlib uses CMake.
If your project already uses CMake, integration should be trivial, through
whatever means you like:
make install + find_package, FetchContent, git submodule + add_subdirectory,
or something else.
There are community-maintained packages available on Conan and on vcpkg.
The scnlib CMake target is scn::scn
# Target with which you'd like to use scnlib
add_executable(my_program ...)
target_link_libraries(my_program scn::scn)See docs for usage without CMake.
A C++17-compatible compiler is required. The following compilers are tested in CI:
Including the following environments:
All times below are in nanoseconds of CPU time. Lower is better.
int)| Test | Test 1 "single" |
Test 2 "repeated" |
Test average |
|---|---|---|---|
scn::scan |
23.8 | 30.4 | 27.1 |
scn::scan_value |
20.5 | 27.4 | 24.0 |
scn::scan_int |
16.5 | 24.1 | 20.3 |
scn::scan_int_exhaustive_valid |
4.08 | - | 4.08 |
std::stringstream |
117 | 53.9 | 85.5 |
sscanf |
71.3 | 474 | 272.7 |
strtol |
16.3 | 23.8 | 20.1 |
std::from_chars |
8.73 | 13.0 | 10.9 |
fast_float::from_chars |
6.87 | 11.8 | 9.35 |
double)| Test | Test 1 "single" |
Test 2 "repeated" |
Test Average |
|---|---|---|---|
scn::scan |
55.8 | 63.7 | 59.7 |
scn::scan_value |
52.1 | 58.8 | 55.5 |
std::stringstream |
294 | 271 | 283 |
sscanf |
159 | 704 | 432 |
strtod |
79.1 | 153 | 116 |
std::from_chars |
18.0 | 28.1 | 23.0 |
fast_float::from_chars |
20.6 | 27.8 | 24.2 |
string and string_view)| Test | |
|---|---|
scn::scan |
24.5 |
scn::scan |
22.2 |
scn::scan_value |
23.1 |
scn::scan_value |
21.0 |
std::stringstream |
134 |
sscanf |
58.4 |
scn::scan is always faster than using stringstreams and sscanfstd::from_chars/fast_float::from_chars is faster than scn::scan, but it
supports fewer featuresstrtod is slower than scn::scan, and supports fewer featuresscn::scan_value is slightly faster compared to scn::scanscn::scan_int is faster than both scn::scan and scn::scan_valuestrtol is ~on-par with scn::scan_int.scn::scan_int_exhaustive_valid is blazing-fast.Above,
stringstream. This test is called "single" in
the benchmark sources."repeated" in the benchmark sources.The difference between "Test 1" and "Test 2" is most pronounced when using
a stringstream, which is relatively expensive to construct,
and seems to be adding around ~50ns of runtime.
With sscanf, it seems like using the %n specifier and skipping whitespace
are really expensive (~400ns of runtime).
With scn::scan and std::from_chars, there's really no state to construct,
and the results for "Test 1" and "Test 2" are thus quite similar.
These benchmarks were run on a Fedora 40 machine, running the Linux kernel version
6.8.9, with an AMD Ryzen 7 5700X processor, and compiled with clang version 18.1.1,
with -O3 -DNDEBUG -march=haswell and LTO enabled.
These benchmarks were run on 2024-05-23 (commit 3fd830de).
The source code for these benchmarks can be found in the benchmark directory.
You can run these benchmarks yourself by enabling the CMake
variable SCN_BENCHMARKS.
This variable is ON by default, if scnlib is the root CMake project,
and OFF otherwise.
$ cd build
$ cmake -DSCN_BENCHMARKS=ON \
-DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \
-DSCN_USE_HASWELL_ARCH=ON ..
$ cmake --build .
# choose benchmarks to run in ./benchmark/runtime/*/*_bench
$ ./benchmark/runtime/integer/scn_int_benchAll sizes below are in kibibytes (KiB), measuring the compiled executable.
"Stripped size" shows the size of the executable after running strip.
Lower is better.
-O3 -DNDEBUG + LTO)Size of scnlib shared library (.so): 1.7M
| Method | Executable size | Stripped size |
|---|---|---|
| empty | 7.6 | 4.4 |
std::scanf |
10.4 | 5.8 |
std::istream |
11.1 | 6.2 |
scn::input |
11.2 | 6.4 |
-Os -DNDEBUG + LTO)Size of scnlib shared library (.so): 1.1M
| Method | Executable size | Stripped size |
|---|---|---|
| empty | 7.5 | 4.4 |
std::scanf |
10.3 | 5.8 |
std::istream |
11.0 | 6.1 |
scn::input |
12.4 | 6.6 |
-g -O0)Size of scnlib shared library (.so): 20M
| Method | Executable size | Stripped size |
|---|---|---|
| empty | 18.4 | 5.2 |
std::scanf |
429 | 11.8 |
std::istream |
438 | 9.4 |
scn::input |
2234 | 51.3 |
When using optimized builds, depending on compiler flags, scnlib provides a
binary, the size of which is within ~5% of what would be produced with scanf
or s. In a Debug-environment, scnlib is ~5x bigger when compared to `scanf` or . After stripping the binaries,
these differences largely go away, except in Debug builds.
In these tests, 25 translation units are generated, in all of which values are
read from stdin five times.
This is done to simulate a small project.
scnlib is linked dynamically, to level the playing field with the standard
library, which is also dynamically linked.
The code was compiled on Fedora 40, with GCC 14.1.1.
See the directory benchmark/binarysize for the source code.
You can run these benchmarks yourself by enabling the CMake
variable SCN_BENCHMARKS_BINARYSIZE.
This variable is ON by default, if scnlib is the root CMake project,
and OFF otherwise.
$ cd build
# For Debug
$ cmake -DCMAKE_BUILD_TYPE=Debug \
-DSCN_BENCHMARKS_BNo open issues yet, or sync has not completed.