String(std::size_t) ambiguous when std::size_t is 64 bit


#1

On build environments where std::size_t is declared as [b]long unsigned int[/b] the following code does not compile:

String toString (std::size_t amount)
{
  return String (amount);
}

#2

The problem is those darn constructors. There should be a templated way to create a String from a specific integer or floating point type. So I added it:

juce_String.h

class String
{
public:
//...
    /** Create a string from a specific number type (integer or floating point)
        If numberOfDecimalPlaces is specified and number is a floating point type,
        the resulting string will have that many decimal places. A value below 0
        means use exponent notation if necessary.
    */
    template <typename Number>
    static String fromNumber (Number number, int numberOfDecimalPlaces = -1);

private:
//....
    struct FromNumber { }; // special constructor tag
    String (CharPointerType text_, FromNumber) : text (text_) { }
};

juce_String.cpp

template <typename Number>
String String::fromNumber (Number number, int)
{
    return String (NumberToStringConverters::createFromInteger <Number> (number), FromNumber ());
}

template <>
String String::fromNumber <float> (float number, int numberOfDecimalPlaces)
{
    if (numberOfDecimalPlaces == 0)
        number = std::floor (number);

    return String (NumberToStringConverters::createFromDouble (
        number, numberOfDecimalPlaces));
}

template <>
String String::fromNumber <double> (double number, int numberOfDecimalPlaces)
{
    if (numberOfDecimalPlaces == 0)
        number = std::floor (number);

    return String (NumberToStringConverters::createFromDouble (
        number, numberOfDecimalPlaces));
}

// Explicit instantiations
template String String::fromNumber <int16> (int16, int);
template String String::fromNumber <int32> (int32, int);
template String String::fromNumber <int64> (int64, int);
template String String::fromNumber <uint16> (uint16, int);
template String String::fromNumber <uint32> (uint32, int);
template String String::fromNumber <uint64> (uint64, int);

Now we have a way to force a String to be produced with a conversion from a specific, named type. This is invaluable when writing templates that take integer types as arguments.


#3

Turns out even MORE surgery is needed… on my gcc std::size_t is declared as long unsigned int, and the calls to numberToString become ambiguous. I’m okay with relying on c++03 features like std::numerical_limits::is_signed so I had to replace all the numberToString with this template:

    // pass in a pointer to the END of a buffer..

    template <typename IntegerType>
    static char* numberToString (char* t, IntegerType const n) noexcept
    {
        if (std::numeric_limits <IntegerType>::is_signed)
        {
            if (n >= 0)
                return printDigits (t, static_cast <uint64> (n));
            // NB: this needs to be careful not to call -std::numeric_limits<int64>::min(),
            // which has undefined behaviour
            t = printDigits (t, static_cast <uint64> (-(n + 1)) + 1);
            *--t = '-';
            return t;
        }
        return printDigits (t, n);
    }

#4

Hmm. Couple of grumbles about this idea: all the NumberToStringConverters was never really intended as public code, so putting that in the header doesn't appeal to me. And annoyingly, using an if statement with is_signed will spit out a lot of compiler warnings about "condition always true", which are hard to get rid of.

Surely a simpler approach would just be to add a String (size_t) constructor on platforms where the size_t type is a distinct type and not just a typedef in a header file? (IIRC it's just MSVC that has can't distinguish between size_t and an unsigned long)


#5

Having too many constructors is a problem. It's a lot better to have a static template member function where you can just pass in the type of integer you want. This is especially helpful when you are writing your own templates, and the integer type comes from an argument. Or a traits_type. Or anything, really. Having only the constructor version is extremely limiting because you have to depend on the compiler to choose the right version.