Proper digit separators in C++

Have you ever hoped that C++ would have digit separators? That you wouldn’t have to strain your eyes when reading 2147483647 (is it std::numeric_limits<int32_t>::max(), or is it just similar)? That you wouldn’t have to count the zeros 5 times when typing 1000000000?

Well, the C++ Standards Committee doesn’t have your back. Oh, sure, they have introduced a digit separator, – , but it’s completely unusable in production code! Here’s why.

Let’s say you’re writing a new feature:

struct answer_provider
{
    static constexpr auto answer() noexcept(true)
    {
        return 42'042;
    }
};

As you can see, my blog’s parser does not handle this separator correctly, and I assure you, neither do many other parsers! Effectively, this means that your new feature’s code will be displayed incorrectly in your organization’s code review tool. And obviously no (even seemingly) incorrect code can ever hope to pass a review.

For an another example, let’s consider the following code:

int main()
{
    auto a =  8'8';
    auto b = u8'8';
    auto c =  8'8;
    auto d = u8'8;
}

What are the types of a, b, c and d? Hint: some of them are actually compiler errors, and you can’t rely on my blog’s syntax colouring.

And that’s why this digit separator is a complete failure.

If only there was a way to use _ as the separator, just like in D, Ruby, Python, C#, Julia, Ada, Rust and even Java…

A library solution!

Fear not, where the committee dropped the ball, I am picking it up! I created a great digit separator library. It’s very simple to use, just include the file:

#include <digit_separators.hpp>

This will allow you to use proper digit separators trivially:

#include <iostream>
 
#include <digit_separators.hpp>
 
int main()
{
        std::cout << 123_456_789 - 12_345_678 << "\n";
}
> g++ foo.cpp -I../../data/include -std=c++17 && ./a.out
111111111

Great, isn’t it? Obviously. But there’s a small caveat: due to buggy compiler implementations, I had to split the implementation of digit separators for billions into separate files. Apparently, they can’t handle large files. Ridiculous, I know, but what can you do?

So, if you want to test larger numbers, you have to use additional headers (header numbers respective to millions):

#include <iostream>
#include <limits>
 
#include <cstdint>
 
#include <digit_separators.hpp>
#include <digit_separators_147.hpp>
 
int main()
{
        static_assert(std::numeric_limits<int32_t>::max() == 2_147_483_647);
}

> clang++ foo.cpp -I../../data/include -std=c++17 && ./a.out

Of course compiles with no errors. What a success!

Where can I get it?

The library is rather large:

Lib size

Unfortunately, GitHub, Bitbucket, GitLab and other git providers proved to be really backwards with their silly repository size limits, so I couldn’t share my library through their services. But you can download it here (full archive) and here (separate headers).

Additionally, here’s the ruby script I’ve used to generate the headers:

#!/usr/bin/ruby
 
def gen_sep(values)
    name = values.map{ |v| '_%03d' % v }.join
    value = "#{1000 ** values.size}ull * value + ";
    value += values
        .reverse
        .each_with_index
        .map{ |v, i| '%3d * %dull' % [v, 1000**i] }
        .reverse
        .join(' + ')
 
    func = 'inline constexpr auto operator""%s(unsigned long long value){ return %s; }'
    func % [name, value]
end
 
def write_file(path, suffix, values, mode = 'w')
    final_path = File.join(File.realpath(path), 'digit_separators%s.hpp' % suffix)
    puts "Creating #{final_path}"
    File.open(final_path, mode) do |f|
        f.puts('#ifndef DIGIT_SEPARATORS%s_HPP' % suffix.upcase)
        f.puts('#define DIGIT_SEPARATORS%s_HPP' % suffix.upcase)
 
        values.each do |v|
            f.puts gen_sep(v)
        end
 
        f.puts('#endif // DIGIT_SEPARATORS%s_HPP' % suffix.upcase)
 
    end
end
 
exit if ARGV.size < 1
 
DEST_PATH = ARGV.first
THOUSAND = (0...1000).to_a
 
def write_main_file(path)
    to_create = THOUSAND.map{ |v| [v] } + THOUSAND.product(THOUSAND)
    write_file(DEST_PATH, '', to_create)
end
 
def write_nth_million(path, n)
    to_create = [n].product(THOUSAND, THOUSAND)
    write_file(DEST_PATH, '_%03d' % n, to_create)
end
 
write_main_file(DEST_PATH)
THOUSAND.each do |n|
    write_nth_million(DEST_PATH, n)
end

5 thoughts on “Proper digit separators in C++

  1. Great information. Since last week, I am gathering details about the c++ experience. There are some amazing details on your blog which I didn’t know. Thanks.

  2. Rust uses single quotation mark as lifetime parameter, and most syntax highlighters learned to handle these correctly (I’ve seen some that still have problems with these, though).

Leave a Reply

Your email address will not be published.