Search code examples
c++localeiostreamnumber-formatting

Performant way of formatting numbers according to the user's locale with iostream?


In one part of our application there is the requirement to format numbers according to the user's locale.

The interface essentially looks like this:

std::string format_number(double number);

The old implementation looked like this:

std::string format_number(double number) {
    using namespace std;
    static const locale user_loc("");
    ostringstream fmtstream;
    fmtstream.imbue(user_loc);
    fmtstream << std::fixed;
    fmtstream << number;
    return fmtstream.str();
}

We have now noticed that with our compiler (MSVC 2005), we can get a approx 10-20% speed up of this function (measured in isolation) by using num_put directly:

std::string format_number(double number) {
    using namespace std;
    static const locale user_loc("");
    ostringstream fmtstream;
    fmtstream.imbue(user_loc);
    fmtstream << std::fixed;
    typedef char* CharBufOutIt;
    typedef num_put<char, CharBufOutIt> np_t;
    np_t const& npf = use_facet<np_t>(user_loc);
    char buf[127];
    const CharBufOutIt begin = &buf[0];
    const CharBufOutIt end = npf.put(/*out=*/begin, /*format=*/fmtstream, /*fill=*/' ', number);
    return std::string(begin, end);
}

This still seems suboptimal:

  • We need to construct a stream object just to imbue it with the locale and use it for the formatting flags.
  • We cannot cache the stream object, as put will call width(0)on the passed format stream.

Is the another "trick" to speed this up, or is this the most we can hope to get out of iostream?

And note that we cannot use sprintf here, as we need to make sure the full locale, including thousands separator, is used, and printf doesn't output the thousand separator.


Solution

  • Thanks to user 0x499602D2's answer I have been able to speed this up some more (even with a locale facet).

    The point is to not use a stream for the formatting flags, since the /*format=*/ parameter to put is only accessed for it's flags and locale, so a more basic object suffices:

    std::string version_3(double number) {
        using namespace std;
        static const locale user_loc("");
        ios fmtios(NULL); // instead of a full stream
        fmtios.imbue(user_loc);
        fmtios.setf(ios_base::fixed);
        typedef char* CharBufOutIt;
        typedef num_put<char, CharBufOutIt> np_t;
        np_t const& npf = use_facet<np_t>(user_loc);
        char buf[127];
        const CharBufOutIt begin = &buf[0];
        const CharBufOutIt end = npf.put(/*out=*/begin, /*format=*/fmtios, /*fill=*/' ', value);
        return std::string(begin, end);
    }
    

    I prematurely had dismissed this version because I was not aware that you can actually legally pass a nullptr as stream buffer to basic_ios. The docs for init show you can, although it will result in badbit being set, but as far as I'm able to tell put doesn't care about the state bits.