Table of Contents

Thanks to the powerful constexpr keyword and many enhancements in recent C++ standards, we can now perform a lot of computations at compile time. In this text, we’ll explore several techniques for parsing integers, including the “naive” approach, C++23, from_chars, std::optional, std::expected, and even some upcoming features in C++26.

But first… why would this even be needed?

Why at compile time?  

While it may sound like a theoretical experiment, since C++11 we can shift more and more computations to compile-time. Here are some key areas and examples where constexpr can be beneficial:

Starting easy from C++17  

Starting with C++17, we are now capable of writing complex constexpr functions. However, our ability to do so is still limited by the range of algorithms available in that context. Luckily, with the introduction of string_view in that version of C++, there is no longer any need to work with “raw” const char* buffers.

Here’s a manual version:

constexpr int parseInt(std::string_view str) {
    const auto start = str.find_first_not_of(WhiteSpaceChars);
    if (start == std::string_view::npos)
        return 0;

    int sign = 1;
    size_t index = start;

    if (str[start] == '-' || str[start] == '+') {
        sign = (str[start] == '-') ? -1 : 1;
        ++index; 
    }

    int val = 0;
    for (; index < str.size(); ++index) {
        char ch = str[index];
        if (ch < '0' || ch > '9') {
            break; 
        }
        val = val * 10 + (ch - '0');
    }

    return sign * val;
}

And the test code:

int main() {
    static_assert(parseInt("123") == 123);
    static_assert(parseInt("-123") == -123);
    static_assert(parseInt("   456   ") == 456);
    static_assert(parseInt("abc123def") == 0);
    static_assert(parseInt("") == 0);
    static_assert(parseInt("abcdef") == 0);
    std::cout << parseInt("9999999999");
}

Run @Compiler Explorer

Okay, the code tests most cases, but one interesting case is left to run at runtime (see std::cout ...). The output is:

1410065407

Although it looks funny, does it actually work correctly?

One of the significant advantages of constexpr functions is that they can run in both “modes,” and this allows for more aggressive testing:

Add this line:

static_assert(parseInt("9999999999") == 1410065407);

And you’ll immediately get the compiler error:

error: non-constant condition for static assertion
   42 |     static_assert(parseInt("9999999999") == 1410065407);
<source>:42:27:   in 'constexpr' expansion ...
<source>:42:42: error: overflow in constant expression [-fpermissive]

From C++ reference - on constant expressions:

A core constant expression is any expression whose evaluation would not evaluate any one of the following:

… 8) an expression whose evaluation leads to any form of core language undefined behavior (including signed integer overflow, division by zero, pointer arithmetic outside array bounds, etc).

To put it simply, when we apply our parseInt function to the argument of "9999999999", it results in an integer overflow. Consequently, the code cannot be executed. Unfortunately, this issue may go unnoticed during runtime, leading to unexpected errors, which can be quite problematic.

To fix the issues, we can use long int to store our intermediate result:

long int val = 0;
for (; index < str.size(); ++index) {
    char ch = str[index];
    if (ch < '0' || ch > '9')
        break; 
        
    val = val * 10 + (ch - '0');
    if (val > numeric_limits<int>::max() || val < numeric_limits<int>::min())
        return 0;
}

See @Compiler Explorer

C++23 - std::from_chars  

Writing some manual parsing code might be fun… and suitable for some experimental code, but it’s best to use a more reliable solution. In C++17, we have from_chars char conversion routines that are super fast and easy to use.

The great thing is that as of C++23, integral overloads for this function, can be used at compile time!

We can use it and swap without limited manual code:

#include <string_view>
#include <charconv> // <<

using namespace std::literals;

constexpr auto WhiteSpaceChars = " \t\n\r\f\v"sv;
constexpr auto NumericCharsMinus = "-1234567890"sv;

constexpr int parseIntCpp23(std::string_view str) {
    const auto start = str.find_first_of(NumericCharsMinus);
    int result{};
    if (start != std::string_view::npos)
    {
        for (size_t i = 0; i < start; ++i)
            if (!WhiteSpaceChars.contains(str[i]))
                return 0;
        std::from_chars(str.data() + start, str.data() + str.size(), result);
    }

    return result;
}

int main() {
    static_assert(parseIntCpp23("123") == 123);
    static_assert(parseIntCpp23("-123") == -123);
    static_assert(parseIntCpp23("   456") == 456);
    static_assert(parseIntCpp23("abc123") == 0);
    static_assert(parseIntCpp23("") == 0);
    static_assert(parseIntCpp23("abcdef") == 0);
    static_assert(parseIntCpp23("99999999999999") == 0);
}

Run @Compiler Explorer

C++23 - std::optional  

Our initial C++23 version returns 0 in case of an error, which is not very informative. Let’s try to improve it.

Fortunately C++23 brings us constexpr support for std::optional (and also std::variant). This feature can improve our parse.

Have a look:

#include <string_view>
#include <charconv> // <<
#include <optional>

using namespace std::literals;

constexpr auto WhiteSpaceChars = " \t\n\r\f\v"sv;
constexpr auto NumericCharsMinus = "-1234567890"sv;

constexpr std::optional<int> tryParseIntCpp23(std::string_view str) {
    const auto start = str.find_first_of(NumericCharsMinus);
    if (start != std::string_view::npos) {
        for (size_t i = 0; i < start; ++i)
            if (!WhiteSpaceChars.contains(str[i]))
                return std::nullopt;
        int result{};
        auto [ptr, ec] = std::from_chars(str.data() + start, 
                                         str.data() + str.size(), result);    
        if (ec == std::errc())
            return result;
    }
    return std::nullopt;
}

int main() {
    static_assert(tryParseIntCpp23("345") == 345);
    static_assert(tryParseIntCpp23("-345") == -345);
    static_assert(tryParseIntCpp23("00") == 0); // <<
    static_assert(tryParseIntCpp23("   6789").value() == 6789);
    static_assert(!tryParseIntCpp23("abc345def").has_value());
    static_assert(!tryParseIntCpp23("").has_value());
    static_assert(!tryParseIntCpp23("abcdef").has_value());
}

See @compiler Explorer

C++23 - std::expected  

And there’s even more!

The version with std::optional conveys more information about the correct conversion, but we can use another handy type to output more data.

std::expected from C++23 is a mix between std::optional and std::variant. It stores the expected value but also has a way to pass error type.

Have a look:

#include <string_view>
#include <charconv> // <<
#include <expected>

using namespace std::literals;

constexpr auto WhiteSpaceChars = " \t\n\r\f\v"sv;
constexpr auto NumericCharsMinus = "-1234567890"sv;

constexpr std::expected<int, std::errc> expectParseIntCpp23(std::string_view str) {
    const auto start = str.find_first_of(NumericCharsMinus);
    if (start != std::string_view::npos) {
        for (size_t i = 0; i < start; ++i)
            if (!WhiteSpaceChars.contains(str[i]))
                return std::unexpected{std::errc::invalid_argument};

        int result{};
        auto [ptr, ec] = std::from_chars(str.data() + start, 
                                         str.data() + str.size(), result);    
        if (ec == std::errc())
            return result;
        
        return std::unexpected { ec };
    }
    return std::unexpected{std::errc::invalid_argument};
}

int main() {
    static_assert(expectParseIntCpp23("567") == 567);
    static_assert(expectParseIntCpp23("-567") == -567);
    static_assert(expectParseIntCpp23("   8910").value() == 8910);
    static_assert(expectParseIntCpp23("9999999999").error() 
                  == std::errc::result_out_of_range);
    static_assert(expectParseIntCpp23("").error() 
                  == std::errc::invalid_argument);
    static_assert(expectParseIntCpp23("mnopqr").error() 
                  == std::errc::invalid_argument);
}

See @Compiler Explorer

As you can see, this time, we not only have a way to pass the result but also the complete error enum type.

C++26  

C++26 is still not feature-ready, but we can get a glimpse of the features and play with them under GCC 14.

There’s at least one feature that can improve our code: Testing for success or failure of <charconv> functions - P2497.

Rather than:

auto res = std::from_chars(str.data() + start, str.data() + str.size(), result);    
if (res.ec == std::errc{}) {...}

We can write:

auto res = std::from_chars(str.data() + start, str.data() + str.size(), result);    
if (res) { ... }

Small, but handy!

Run @Compiler Explorer - uses GCC trunk, GCC 14 with this feature already available in the Standard Library.

Summary  

In this text, I showed you an example of parsing strings into numbers at compile time. We started from some “manual” parsers in C++17 and then extended it though C++23 and even C++26 features.

As you can notice, C++23 brings many constexpr improvements and allows us to use vocabulary types.

I’m curious about more C++26 features, as we might get “#embed,” which enables working with standalone files at compile time! Parsing configurations, jsons, or other data will be even nicer!

Please note that the parsing code in this article is still not production ready, and there are some cases which I skipped.

Back to you

  • Do you parse strings at compile time?
  • When do you use constexpr functions?

Share your comments below.