Language parsing C++ input io
Need help with scnlib?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.
eliaskosunen

Description

scanf for modern C++

321 Stars 8 Forks Apache License 2.0 345 Commits 4 Opened issues

Services available

Need anything else?

scnlib

Travis CI Build Status Appveyor CI Build Status Codecov Coverage Codacy Code Quality Latest Release License C++ Standard

#include 
#include 

int main() { int i; // Read an integer from stdin // with an accompanying message scn::prompt("What's your favorite number? ", "{}", i); printf("Oh, cool, %d!", i); }

// Example result: // What's your favorite number? 42 // Oh, cool, 42!

What is this?

scnlib
is a modern C++ library for replacing
scanf
and
std::istream
. This library attempts to move us ever so closer to replacing
iostream
s and C stdio altogether. It's faster than
iostream
(see Benchmarks) and type-safe, unlike
scanf
. Think {fmt} but in the other direction.

This library is the reference implementation of the ISO C++ standards proposal P1729 "Text Parsing".

This library is currently of pre-release quality (version 0.3). It is usable in its present state, but do not expect a bug-free experience. Additionally, I reserve the right to change the interfaces in incompatible ways in future minor version releases (e.g. 0.4).

The next release of this library will be 0.4. After that, unless significant design flaws can be found, the next major release will be 1.0-rc1. 1.0 will be released when an rc-version will prove itself reasonably bug-free.

Documentation

The documentation can be found online, from https://scnlib.readthedocs.io.

To build the docs yourself, build the

doc
and
doc-sphinx
targets generated by CMake. The
doc
target requires Doxygen, and
doc-sphinx
requires Python 3.8, Sphinx and Breathe.

Examples

Reading a
std::string

#include 
#include 
#include 

int main() { std::string word; auto result = scn::scan("Hello world", "{}", word);

std::cout << word << '\n'; // Will output "Hello"
std::cout << result.string() << '\n';  // Will output " world!"

}

Reading multiple values

#include 

int main() { int i, j; auto result = scn::scan("123 456 foo", "{} {}", i, j); // result == true // i == 123 // j == 456

std::string str;
ret = scn::scan(ret.range(), "{}", str);
// result == true
// str == "foo"

}

Using the
tuple
-return API

#include 
#include 

int main() { auto [r, i] = scn::scan_tuple("42", "{}"); // r is a result object, contextually convertible to bool // i == 42 }

Error handling

#include 
#include 
#include 

int main() { int i; // "foo" is not a valid integer auto result = scn::scan("foo", "{}", i); if (!result) { // i is not touched (still unconstructed) // result.range() == "foo" (range not advanced) std::cout << "Integer parsing failed with message: " << result.error().msg() << '\n'; } }

Features

  • Blazing-fast parsing of values (see benchmarks)
  • Modern C++ interface, featuring type safety (variadic templates), convenience (ranges) and customizability
    • No << chevron >> hell
    • Requires C++11 or newer
  • "{python}"-like format string syntax
  • Optionally header only
  • Minimal code size increase (see benchmarks)
  • No exceptions (supports building with
    -fno-exceptions -fno-rtti
    with minimal loss of functionality)
    • Localization requires exceptions, because of the way
      std::locale
      is

Installing

scnlib
uses CMake. If your project already uses CMake, integration is easy. First, clone, build, and install the library
# Whereever you cloned scnlib to
$ mkdir build
$ cd build
$ cmake ..
$ make -j
$ make install

Then, in your project:

# Find scnlib package
find_package(scn CONFIG REQUIRED)

Target which you'd like to use scnlib

scn::scn-header-only to use the header-only version

add_executable(my_program ...) target_link_libraries(my_program scn::scn)

See docs for usage without CMake.

Compiler support

Every commit is tested with * gcc 5.5 and newer (until v10) * clang 3.7 and newer (until v11) * Visual Studio 2017 and 2019 * gcc 10 on ARM, and clang on macOS

with very extreme warning flags (see cmake/flags.cmake) and with multiple build configurations for each compiler.

Other compilers and compiler versions may work, but it is not guaranteed. If your compiler does not work, it may be a bug in the library. However, support will not be provided for:

  • GCC 4.9 (or earlier): C++11 support is too buggy
  • VS 2015 (or earlier): unable to handle templates

Benchmarks

Run-time performance

Benchmark results

These benchmarks were run on a Ubuntu 20.04 machine running kernel version 5.4.0-52, with an Intel Core i5-6600K processor, and compiled with gcc version 9.3.0, with

-O3 -DNDEBUG -march=native
. The source code for the benchmarks can be seen in the
benchmark
directory.

You can run the benchmarks yourself by enabling

SCN_BUILD_BENCHMARKS
.
SCN_BUILD_BENCHMARKS
is enabled by default if
scn
is the root CMake project, and disabled otherwise.
$ cd build
$ cmake -DCMAKE_BUILD_TYPE=Release -DSCN_BUILD_BENCHMARKS=ON -DSCN_NATIVE_ARCH=ON -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON ..
$ make -j
# choose benchmark to run in ./benchmark/runtime/*/bench-*
$ ./benchmark/runtime/integer/bench-int

Performance comparison benchmarks with Boost.Spirit.x3 can be found here

Times are in nanoseconds of CPU time. Lower is better.

Integer parsing (
int
)

| Test |

std::stringstream
|
sscanf
|
scn::scan
|
scn::scan_default
| | :----- | ------------------: | -------: | ----------: | ------------------: | | Test 1 | 274 | 96.5 | 43.0 | 40.3 | | Test 2 | 77.7 | 526 | 68.1 | 60.5 |

Floating-point parsing (
double
)

| Test |

std::stringstream
|
sscanf
|
scn::scan
| | :----- | ------------------: | -------: | ----------: | | Test 1 | 416 | 164 | 167 | | Test 2 | 223 | 570 | 195 |

Reading random whitespace-separated strings

| Character type |

scn::scan
|
scn::scan
and
string_view
|
std::stringstream
| | :------------- | ----------: | ----------------------------: | ------------------: | |
char
| 40.7 | 38.0 | 50.2 | |
wchar_t
| 42.7 | 38.3 | 122 |

Test 1 vs. Test 2

In the above comparisons:

  • "Test 1" refers to parsing a single value from a string which only contains the string representation for that value. The time used for constructing parser state is included. For example, the source string could be
    "123"
    . In this case, a parser is constructed, and a value (
    123
    ) is parsed. This test is called "single" in the benchmark sources.
  • "Test 2" refers to the average time of parsing a value from a string containing multiple string representations separated by spaces. The time used for constructing parser state is not included. For example, the source string could be
    "123 456"
    . In this case, a parser is constructed before the timer is started. Then, a single value is read from the source, and the source is advanced to the start of the next value. The time it took to parse a single value is averaged out. This test is called "repeated" in the benchmark sources.

Code size

Code size benchmarks test code bloat for nontrivial projects. It generates 25 translation units and reads values from stdin five times to simulate a medium sized project. The resulting executable size is shown in the following tables.

The code was compiled on Ubuntu 20.04 with g++ 9.3.0.

scnlib
is linked dynamically to level out the playing field compared to already dynamically linked
libc
and
libstdc++
. See the directory
benchmark/bloat
for more information, e.g. templates for each TU.

To run these tests yourself:

$ cd build
# For Debug
$ cmake -DCMAKE_BUILD_TYPE=Debug -DSCN_BLOAT=ON -DBUILD_SHARED_LIBS=ON -DSCN_INSTALL=OFF ..
# For Release
$ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -DSCN_BLOAT=ON -DBUILD_SHARED_LIBS=ON -DSCN_INSTALL=OFF ..
# For Minimized Release
$ cmake -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -DSCN_BLOAT=ON -DBUILD_SHARED_LIBS=ON -DSCN_INSTALL=OFF ..

$ make -j $ ./benchmark/bloat/run-bloat-tests.py ./benchmark/bloat

Sizes are in kibibytes (KiB). Lower is better.

Minimized build (-Os -DNDEBUG)

| Method | Executable size | Stripped size | | :-------------------------- | --------------: | ------------: | | empty | 18 | 14 | |

scanf
| 23 | 18 | |
std::istream
/
std::cin
| 25 | 18 | |
scn::input
| 35 | 30 | |
scn::input
(header only) | 138 | 98 |

Release build (-O3 -DNDEBUG)

| Method | Executable size | Stripped size | | :-------------------------- | --------------: | ------------: | | empty | 18 | 14 | |

scanf
| 24 | 18 | |
std::istream
/
std::cin
| 30 | 22 | |
scn::input
| 41 | 34 | |
scn::input
(header only) | 177 | 146 |

Debug build (-g)

| Method | Executable size | Stripped size | | :-------------------------- | --------------: | ------------: | | empty | 29 | 14 | |

scanf
| 600 | 18 | |
std::istream
/
std::cin
| 662 | 22 | |
scn::input
| 1709 | 51 | |
scn::input
(header only) | 6858 | 281 |

Build time

This test measures the time it takes to compile a binary when using different libraries. Note, that the time it takes to compile the library is not taken into account (unfair measurement against precompiled stdlibs).

These tests were run on an Ubuntu 20.04 machine with an i5-6600K and 16 GB of RAM, using GCC 9.3.0. The compiler flags for a debug build were

-g
, and
-O3 -DNDEBUG
for a release build.

To run these tests yourself, enable CMake flag

SCN_BUILD_BUILDTIME
. In order for these tests to work,
c++
must point to a gcc-compatible C++ compiler binary, and a POSIX-compatible
/usr/bin/time
must be present.
$ cd build
$ cmake -DSCN_BUILD_BUILDTIME=ON ..
$ make -j
$ ./benchmark/buildtime/run-buildtime-tests.sh

Build time

Time is in seconds of CPU time (user time + sys/kernel time). Lower is better.

| Method | Debug | Release | | :-------------------------- | ----: | ------: | | empty | 0.03 | 0.04 | |

scanf
| 0.24 | 0.25 | |
std::istream
/
std::cin
| 0.29 | 0.31 | |
scn::input
| 0.53 | 0.62 | |
scn::input
(header only) | 1.38 | 2.54 |

Memory consumption

Memory is in mebibytes (MiB). Lower is better.

| Method | Debug | Release | | :-------------------------- | ----: | ------: | | empty | 22.3 | 23.9 | |

scanf
| 47.0 | 46.7 | |
std::istream
/
std::cin
| 55.2 | 54.7 | |
scn::input
| 82.9 | 83.9 | |
scn::input
(header only) | 143.1 | 167.6 |

Acknowledgements

The contents of this library are heavily influenced by {fmt} and its derivative works.

https://github.com/fmtlib/fmt
https://fmt.dev

{fmt} is licensed under the MIT license.
Copyright (c) 2012-present Victor Zverovich

The ranges implementation found from this library is based on NanoRange:

https://github.com/tcbrindle/NanoRange

NanoRange is licensed under the Boost Software License, version 1.0.
Copyright (c) 2018 Tristan Brindle

License

scnlib is licensed under the Apache License, version 2.0.
Copyright (c) 2017 Elias Kosunen
See LICENSE for further details

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.