2
\$\begingroup\$

C++17 have <charconv>. However this header does not work on MacOS - It compiles, but can not link. Same header is not present in MSVS. The header also missing from my Arm Linux, even my gcc compiler there supports C++17 as well.

Following is a take to convert non terminated char array (e.g. buffer, string_view etc) into signed or unsigned integer.

I probably will use the code in production, once I do some more tests.

#include <limits>
#include <type_traits>
#include <string_view>
#include <utility>

namespace impl{
    template<typename T>
    T x10(T a){
        return a * 10;
    }

    inline char digit(char c){
        return c - '0';
    }

    inline bool is_space(char c){
        return c == ' ' || c == '\t' || c == '\r' || c == '\n';
    }

    inline bool is_sign(char c){
        return c == '+' || c == '-';
    }

    inline bool is_digit(char c){
        return c >= '0' && c <= '9';
    }

    template<typename T>
    std::pair<bool,T> convert_unsigned(const char *c, const char *e){
        T num = 0;

        while(c != e && is_digit(*c)){
            T tmp = x10(num) + digit(*c);

            if (tmp < num)
                return { false, num };

            num = tmp;

            ++c;
        }

        return { true, num };
    }

    template<typename T>
    std::pair<bool,T> convert(const char *c, const char *e, std::true_type /* unsigned */){
        if (c != e && *c == '+')
            ++c;

        return convert_unsigned<T>(c, e);
    }

    template<typename T>
    std::pair<bool,T> convert(const char *c, const char *e, std::false_type /* unsigned */){
        using U = std::make_unsigned_t<T>;

        T sign = +1;
        U max  = std::numeric_limits<T>::max();

        if (c != e && is_sign(*c)){
            if (*c == '-'){
                sign = -1;
                max  = - (U) std::numeric_limits<T>::min();
            }

            ++c;
        }

        // std::cout << ">>> " << sign << ' ' << max << ' ' << '\n';

        auto [ ok, result ] = convert_unsigned<U>(c, e);

        if (result > max)
            return { false, (T) result };

        if (result == max)
            return { true, (T) result };

        return { ok, sign * (T) result };
    }
}

template<typename T>
std::pair<bool,T> convert(std::string_view const s){
    static_assert(std::is_integral_v<T>);

    return impl::convert<T>(std::begin(s), std::end(s), std::is_unsigned<T>{});
}

#include <cstdint>
#include <iostream>

template<typename T>
void test(const char *s, T correct){
    auto [ok, i] = convert<T>(s);

    if (!ok)
        std::cout << "Err" << '\n';

    std::cout << ( i == correct ? "OK" : "NO") << ' ' << i << '\n';
}

int main(){
    test<uint16_t>  ("-0",              std::numeric_limits<uint16_t>::min()    );
    test<uint16_t>  ("65535",           std::numeric_limits<uint16_t>::max()    );
    test<int16_t>   ("+32767",          std::numeric_limits<int16_t>::max() );
    test<int16_t>   ("-32768",          std::numeric_limits<int16_t>::min() );
    test<int16_t>   ("+0",              0                   );
    test<int16_t>   ("-0",              0                   );
    test<int16_t>   ("+10",             +10                 );
    test<int16_t>   ("-10",             -10                 );

    test<uint64_t>  ("+0",                      std::numeric_limits<uint64_t>::min()    );
    test<uint64_t>  ("+18446744073709551615",   std::numeric_limits<uint64_t>::max()    );
    test<int64_t>   ("+9223372036854775807",    std::numeric_limits<int64_t>::max() );
    test<int64_t>   ("-9223372036854775808",    std::numeric_limits<int64_t>::min() );
}
\$\endgroup\$
1
  • \$\begingroup\$ Added some nodes why someone needs these functions. \$\endgroup\$ – Nick Nov 22 '19 at 21:14
1
\$\begingroup\$

We're missing any documentation or template constraints, but it appears that this code parses only decimal integers, and silently ignores anything after the first non-digit. There are other departures from the interface of the functions provided in <charconv>:

  • a leading + is accepted and ignored
  • not all out-of-range values are detected.

Worse, there's no prevention of signed integer overflow, so there's undefined behaviour on certain inputs.
The avoidance of signed overflow is fragile and could be made clearer to the reader (with suitable static_assert() if requires isn't palatable, for instance).

I think that writing a function to multiply by 10 is taking functional decomposition a step too far. I would say the same for subtracting '0', but that could be arguable if it were home for a comment explaining how C++ guarantees that the characters '0'..'9' are encoded as contiguous values.

There are no tests for any of the failure cases, and all the tests have to be checked externally. It's better to make main() return an error status unless all the tests pass (and diagnostic output should go to std::clog or std::cerr as appropriate, rather than std::cout).

\$\endgroup\$
4
  • \$\begingroup\$ show me input where i have UB \$\endgroup\$ – Nick Nov 25 '19 at 16:15
  • \$\begingroup\$ T tmp = x10(num) + digit(*c); - (...time passes...) ah, that's unreachable with signed T - but not obviously so, and it's fragile (signedness passed in a bool). \$\endgroup\$ – Toby Speight Nov 25 '19 at 16:49
  • \$\begingroup\$ >> if it were home for a comment explaining how C++ guarantees that the characters '0'..'9' are encoded as contiguous values. this is guaranteed from ASCII. also lowest 3 bits is the number. \$\endgroup\$ – Nick Nov 25 '19 at 18:25
  • \$\begingroup\$ I know it's guaranteed by ASCII, but there's nothing that forces C++ implementations to use ASCII. That's why the comment explaining that C++ does require contiguous digits is valuable. \$\endgroup\$ – Toby Speight Nov 25 '19 at 18:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.