0

Backstory

I am developing a TFT LCD library (that I will open source once I finish) and came to the understanding I need a lightweight itoa and fptoa function to convert integral and floating point values to strings. After reading through quit a few posts here I didn't find a concrete implementation here so I decided to post it myself.

Limitation

  1. no dynamic allocations
  2. c++11 compatible.

The problem

The first approach was to look if there is a solution in the standard library and sadly itoa isn't part of the standard and fptoa doesn't exist. However, there is still sprintf right ? The problem is the overhead for all of the internal checks it does and the other c++ methods aren't much more appealing:

  1. std::string - heavy + dynamic memory usage (If number is too big for short string optimization).
  2. Streams - Too slow and heavy.
globalturist
  • 71
  • 1
  • 7
  • Why not anything from [this SO question about fptoa](https://stackoverflow.com/questions/7228438/convert-double-float-to-string) or this about [itoa](https://stackoverflow.com/questions/8257714/how-to-convert-an-int-to-string-in-c)? – nada May 04 '20 at 12:15
  • 1
    I'd benchmark your alternative solution against [`std::to_string`](https://en.cppreference.com/w/cpp/string/basic_string/to_string) and [`std::sprintf`](https://en.cppreference.com/w/cpp/io/c/fprintf) before concluding that the standard functionality is too slow. You might be surprised. – Sander De Dycker May 04 '20 at 12:28
  • @nada there is no nan and inf checks in "this SO question about fptoa" and the solutions aren't readable and bound to float only. as for itoa its not part of the standard and the sprintf has a formatting parameter and will produce bigger code than the function i made. – globalturist May 04 '20 at 13:35
  • @SanderDeDycker I tested what you said and my code generates a smaller size than std::sprintf. Also, my problem with std::to_string is that it returns an std::string object whereas if the number we converted is big doesn't guarantee small string optimization and will allocate heap memory which is against one of the limitations i written. – globalturist May 04 '20 at 13:41
  • I hadn't realized your intent was to optimize for code size as opposed to speed. – Sander De Dycker May 04 '20 at 13:53
  • @SanderDeDycker well it is for embedded systems where memory is at a premium – globalturist May 04 '20 at 14:23
  • The sound solution is to avoid C++ for embedded systems and C++11 in particular. All the problems you describe are there because the presence of C++. – Lundin May 06 '20 at 06:40
  • @SanderDeDycker I've done such benchmarks before on embedded targets. sprintf produces roughly 20 times the code size and 10 times slower execution than a simplistic home-made integer-to-string. You simply shouldn't use anything from stdio.h in embedded systems (or elsewhere). As for std::string, it can't be used because of implicit heap allocations. And all C++ classes give "constructor lag" at start-up, which can't be avoided if you use classes and static storage duration objects. – Lundin May 06 '20 at 06:52
  • @Lundin Your answer is opinion based. Thank you for your time. – globalturist May 06 '20 at 06:56
  • @globalturist It's a comment, an it's experience-based from over 20 years of using C++. – Lundin May 06 '20 at 07:02
  • @Lundin "The sound solution is to avoid C++ for embedded systems" is just an opinion. watch some presentations from cppcon where its clearly shown c++ is as fast and in some cases faster than c++ and also provides type safety and more bug free production code. look there up for example: [link](https://www.youtube.com/watch?v=LfRLQ7IChtg&t=1549s), [link](https://www.youtube.com/watch?v=PDSvjwJ2M80&t=435s). – globalturist May 06 '20 at 07:07
  • @globalturist And why is it that the C++ cult must keep "proving" that the language is as fast as C, year after year since the dawn of time. I was a big C++ fan long time ago and even used it in several embedded projects when I was a confused rookie. This was around the time when EC++ was launched in an attempt to save the language, but the PC programmers stomped that out. As so we are stuck with a language where some ~80% of all available language features are dangerous bloat in the context of embedded systems and the vast majority of C++ programmers can't tell which ones they are. – Lundin May 06 '20 at 07:41
  • @Lundin I didn't make this post to argue which language should be used in embedded systems as it is opinion based. I don't mean to disrespect you but it would be great if you kept your opinion to yourself and replied only if you have an offer to improve the code. – globalturist May 06 '20 at 09:16
  • @globalturist Here's the improved code then: `uint_fast8_t i; for(i=1; i<=length; i++) { str[length-i] = val % 10UL + '0'; val/=10; } str[i-1] = '\0';`. It completely lacks superfluous features and bloat. Should you need sign support, different radix etc then implement a separate function for that. – Lundin May 06 '20 at 09:22

1 Answers1

0

itoa solution

#include<algorithm>
#include<type_traits>
#include<cstdint>

static constexpr char digits[]{ "0123456789ABCDEF" };

enum class Base : uint8_t
{
    BIN = 2,
    OCT = 8,
    DEC = 10,
    HEX = 16
};

template<typename T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
char* itoa(T number, char* buf, const Base base) noexcept
{
    if (base < Base::BIN || base > Base::HEX) {
        *buf = '\0';
        return buf;
    }

    bool negative{ false };
    if (number < 0 && base == Base::DEC) {
        number = -number;
        negative = true;
    }

    typename std::make_unsigned<T>::type unsigned_number = number;
    uint8_t index{ 0 };
    do {
        buf[index++] = digits[unsigned_number % static_cast<decltype(unsigned_number)>(base)];
        unsigned_number /= static_cast<decltype(unsigned_number)>(base);
    } while (unsigned_number);

    if (negative) {
        buf[index++] = '-';
    }

    std::reverse(buf, buf + index);

    buf[index] = '\0';

    return &buf[index];
}

ftoa solution

This one was a bit more tricky as I had to create a type that ensures the precision value will be in bounds:

#include<cfloat>
#include<cstdint>

template<uint8_t v, uint8_t min, uint8_t max>
struct BoundIntegral
{
    static_assert(min < max, "min bound must be lower than max");
    static_assert(v >= min && v <= max, "value out of bounds");
    static constexpr uint8_t value{ v };
};

constexpr uint8_t MIN_PRECISION{ 1 };

template<typename T, uint8_t prec> struct Precision {};

template<uint8_t prec>
struct Precision<long double, prec> : BoundIntegral<prec, MIN_PRECISION, LDBL_DIG> {};

template<uint8_t prec>
struct Precision<double, prec> : BoundIntegral<prec, MIN_PRECISION, DBL_DIG> {};

template<uint8_t prec>
struct Precision<float, prec> : BoundIntegral<prec, MIN_PRECISION, FLT_DIG> {};

Next up was the actual fptoa implementation:

#include<cstdint>
#include<cmath>
#include<type_traits>

constexpr uint8_t ERR_LEN{ 3 };

template<typename T, uint8_t prec, typename = typename std::enable_if<std::is_floating_point<T>::value>::type>
char* fptoa(T number, char* buf) noexcept
{
    auto memcpy = [](char* dest, const char* const src, uint8_t length) noexcept {
        for (uint8_t index{}; index < length; ++index) {
            dest[index] = src[index];
        }
        return &dest[length];
    };

    if (std::isnan(number)) {
        buf = memcpy(buf, "NAN", ERR_LEN);
    }
    else if (std::isinf(number)) {
        buf = memcpy(buf, "INF", ERR_LEN);
    }
    else if (number > INT32_MAX) {
        buf = memcpy(buf, "OVF", ERR_LEN);
    }
    else if (number < INT32_MIN) {
        buf = memcpy(buf, "OVF", ERR_LEN);
    }
    else {
        T rounding = 0.5 / std::pow(10.0, Precision<T, prec>::value);
        if (number < 0.0) {
            number -= rounding;
        }
        else {
            number += rounding;
        }

        int32_t integral_part = static_cast<int32_t>(number);
        buf = itoa(integral_part, buf, Base::DEC);

        *buf++ = '.';

        T fractional_part = std::abs(number - static_cast<T>(integral_part));
        uint8_t precision = Precision<T, prec>::value;
        while (precision--) {
            fractional_part *= 10;
            *buf++ = digits[static_cast<uint8_t>(fractional_part)];
            fractional_part -= static_cast<uint8_t>(fractional_part);
        }
    }

    *buf = '\0';

    return buf;
}

Use case

For the use case itself I made a utility template that can deduce the buffer size I will need:

#include<climits>
#include<type_traits>
#include<cstdint>

template<typename T>
struct bits_constant : std::integral_constant<std::size_t, sizeof(T) * CHAR_BIT> {};

And use it as following:

#include<cstdio>

int main()
{
    char fpbuf[bits_constant<double>::value + 1];
    double d{ 3.123 };
    fptoa<double, 3>(3.123, fpbuf); //convert double value with 3 digits precision

    std::printf("%s", fpbuf);

    char ibuf[bits_constant<int32_t>::value + 1];
    int32_t i{-255};
    itoa(i, ibuf, Base::DEC);

    return 0;
}

Summary

Feel free to use the code in your project. And I would appreciate it if you could suggest improvements to the code, as the goal of this post is to help others use a portable solution to converting integral and floating point values to strings.

Godbolt link for assembly output: https://godbolt.org/z/PGiw7m

globalturist
  • 71
  • 1
  • 7
  • Oh my: `buf[index++] = digits[unsigned_number % static_cast(base)];` also a no-no for me: `*buf++` – nada May 04 '20 at 12:17
  • @nada `buf[index++] = digits[unsigned_number % static_cast(base)];` - It is kind of verbose. However because `base` is an `enum class` i must cast it. the only option would be what i did above and I'l gladly explain the rationale if you are interested. About `*buf++` you do know that many of the standard libraries use this right ? also its there to avoid allocating an index variable. – globalturist May 04 '20 at 13:44
  • @nada Also, if you have any different approach you would recommend I would like to hear about it. Thank you for your time! – globalturist May 04 '20 at 13:47
  • Yes: Always put readability above any other concerns. Especially in hard-to-debug embedded environments. Personally I wouldn't use your code, because it could well be an entrant for the underhanded C contest. I'm sorry for putting it so harsh. – nada May 04 '20 at 14:32
  • @nada I appreciate your honesty. However, than you wouldn't use any standard library that uses raw pointers as it should also be considered "an entrant for the underhanded C contest" look at the algorithm library which has heavy use of pointers inside of it e.g. [link](https://en.cppreference.com/w/cpp/algorithm/find_first_of). Again I do understand its a matter of style but you didn't recommend another approach and I don't really get why de-referencing a pointer and post increment is that unreadable. – globalturist May 04 '20 at 14:54
  • Ask a C++ developer on the street which operator has precedence over the other. Hardly anyone knows. I'd at least put brackets around the buf++. – nada May 04 '20 at 18:20
  • You probably shouldn't be using templates since these functions are specialized. You might be able to drop some of those casts if you do. Consider overloading specific functions for the various integer or float types instead. – Lundin May 06 '20 at 06:36