c++boostnumber-formattingstandard-libraryboost-lexicalcast

Boost's lexical_cast From double to string Precision


I'm working with a library that unfortunately uses boost::lexical_cast to convert from a double to a string.

I need to be able to definitively mirror that behavior on my side, but I was hopping to do so without propagating boost.

Could I be guaranteed identical behavior using to_string, sprintf, or some other function contained within the standard?


Solution

  • The boost code ends up here:

                bool shl_real_type(double val, char* begin) {
                    using namespace std;
                    finish = start +
    #if defined(_MSC_VER) && (_MSC_VER >= 1400) && !defined(__SGI_STL_PORT) && !defined(_STLPORT_VERSION)
                        sprintf_s(begin, CharacterBufferSize,
    #else
                        sprintf(begin, 
    #endif
                        "%.*g", static_cast<int>(boost::detail::lcast_get_precision<double>()), val);
                    return finish > start;
                }
    

    You're in luck since the precision is USUALLY compile-time constant (unless boost configures BOOST_LCAST_NO_COMPILE_TIME_PRECISION).

    Simplifying a bit and allowing for conforming, modern standard libraries:

    Mimicking Boost Lexicalcast

    #include <cstdio>
    #include <limits>
    #include <string>
    
    namespace {
        template <class T> struct lcast_precision {
            typedef std::numeric_limits<T> limits;
    
            static constexpr bool use_default_precision  = !limits::is_specialized || limits::is_exact;
            static constexpr bool is_specialized_bin     = !use_default_precision && limits::radix == 2 && limits::digits > 0;
    
            static constexpr bool is_specialized_dec     = !use_default_precision && limits::radix == 10 && limits::digits10 > 0;
            static constexpr unsigned int precision_dec  = limits::digits10 + 1U;
            static constexpr unsigned long precision_bin = 2UL + limits::digits * 30103UL / 100000UL;
    
            static constexpr unsigned value = is_specialized_bin 
                ? precision_bin 
                : is_specialized_dec? precision_dec : 6;
        };
    
        std::string mimicked(double v) {
            constexpr int prec = static_cast<int>(lcast_precision<double>::value);
    
            std::string buf(prec+10, ' ');
            buf.resize(sprintf(&buf[0], "%.*g", prec, v));
            return buf;
        }
    }
    

    Regression Tests

    To compare the results and check the assumptions:

    Live On Coliru

    #include <cassert>
    #include <cstdio>
    #include <limits>
    #include <string>
    
    namespace {
        template <class T> struct lcast_precision {
            typedef std::numeric_limits<T> limits;
    
            static constexpr bool use_default_precision  = !limits::is_specialized || limits::is_exact;
            static constexpr bool is_specialized_bin     = !use_default_precision && limits::radix == 2 && limits::digits > 0;
    
            static constexpr bool is_specialized_dec     = !use_default_precision && limits::radix == 10 && limits::digits10 > 0;
            static constexpr unsigned int precision_dec  = limits::digits10 + 1U;
            static constexpr unsigned long precision_bin = 2UL + limits::digits * 30103UL / 100000UL;
    
            static constexpr unsigned value = is_specialized_bin 
                ? precision_bin 
                : is_specialized_dec? precision_dec : 6;
        };
    
        std::string mimicked(double v) {
            constexpr int prec = static_cast<int>(lcast_precision<double>::value);
    
            std::string buf(prec+10, ' ');
            buf.resize(sprintf(&buf[0], "%.*g", prec, v));
            return buf;
        }
    }
    
    #include <cmath>
    #include <iomanip>
    #include <iostream>
    #include <numbers>
    #include <string>
    
    #include <boost/lexical_cast.hpp>
    
    #ifdef BOOST_LCAST_NO_COMPILE_TIME_PRECISION
    #error BOOM
    #endif
    
    #define TEST(x)                                                                                                        \
        do {                                                                                                               \
            std::cout << std::setw(45) << #x << ":\t" << (x) << "\n";                                                      \
        } while (0)
    
    std::string use_sprintf(double v) {
        std::string buf(32, ' ');
        buf.resize(std::sprintf(&buf[0], "%f", v));
        return buf;
    }
    
    static std::locale DE("de_DE.utf8");
    static double const cases[] = {
        std::numeric_limits<double>::quiet_NaN(),
        std::numeric_limits<double>::infinity(),
        -std::numeric_limits<double>::infinity(),
        0.0,
        -0.0,
        std::numeric_limits<double>::epsilon(),
        std::numbers::pi,
    };
    
    ////////////////////////////////////////////////////////////////
    // additional tests for std::format and libfmt (2024)
    // std::format and libfmt are locale-independent by default
    #include <format>
    std::string use_std_format(double v) { return std::format("{:f}", v); }
    std::string use_std_format_locale(double v) { return std::format("{:L}", v); }
    
    #include <fmt/core.h>
    std::string use_libfmt(double v) { return fmt::format("{:f}", v); }
    std::string use_libfmt_locale(double v) { return fmt::format("{:L}", v); }
    ////////////////////////////////////////////////////////////////
    
    void tests() {
        for (double v : cases) {
            TEST(v);
            TEST(std::to_string(v));
            TEST(use_sprintf(v));
            TEST(boost::lexical_cast<std::string>(v));
            TEST(mimicked(v));
            // TEST(use_std_format(v));
            TEST(use_std_format_locale(v)); // 2024
            // TEST(use_libfmt(v));
            TEST(use_libfmt_locale(v)); // 2024
    
            assert(mimicked(v) == boost::lexical_cast<std::string>(v));
        }
    }
    
    int main() {
        tests();
    
        std::cout << "==== imbue std::cout\n";
        std::cout.imbue(DE);
    
        tests();
    
        std::cout << "==== override global locale\n";
        std::locale::global(DE);
    
        tests();
    }
    

    Prints

                                                v:  nan
                                std::to_string(v):  nan
                                   use_sprintf(v):  nan
              boost::lexical_cast<std::string>(v):  nan
                                      mimicked(v):  nan
                         use_std_format_locale(v):  nan
                             use_libfmt_locale(v):  nan
                                                v:  inf
                                std::to_string(v):  inf
                                   use_sprintf(v):  inf
              boost::lexical_cast<std::string>(v):  inf
                                      mimicked(v):  inf
                         use_std_format_locale(v):  inf
                             use_libfmt_locale(v):  inf
                                                v:  -inf
                                std::to_string(v):  -inf
                                   use_sprintf(v):  -inf
              boost::lexical_cast<std::string>(v):  -inf
                                      mimicked(v):  -inf
                         use_std_format_locale(v):  -inf
                             use_libfmt_locale(v):  -inf
                                                v:  0
                                std::to_string(v):  0.000000
                                   use_sprintf(v):  0.000000
              boost::lexical_cast<std::string>(v):  0
                                      mimicked(v):  0
                         use_std_format_locale(v):  0
                             use_libfmt_locale(v):  0
                                                v:  -0
                                std::to_string(v):  -0.000000
                                   use_sprintf(v):  -0.000000
              boost::lexical_cast<std::string>(v):  -0
                                      mimicked(v):  -0
                         use_std_format_locale(v):  -0
                             use_libfmt_locale(v):  -0
                                                v:  2.22045e-16
                                std::to_string(v):  0.000000
                                   use_sprintf(v):  0.000000
              boost::lexical_cast<std::string>(v):  2.2204460492503131e-16
                                      mimicked(v):  2.2204460492503131e-16
                         use_std_format_locale(v):  2.220446049250313e-16
                             use_libfmt_locale(v):  2.220446049250313e-16
                                                v:  3.14159
                                std::to_string(v):  3.141593
                                   use_sprintf(v):  3.141593
              boost::lexical_cast<std::string>(v):  3.1415926535897931
                                      mimicked(v):  3.1415926535897931
                         use_std_format_locale(v):  3.141592653589793
                             use_libfmt_locale(v):  3.141592653589793
    ==== imbue std::cout
                                                v:  nan
                                std::to_string(v):  nan
                                   use_sprintf(v):  nan
              boost::lexical_cast<std::string>(v):  nan
                                      mimicked(v):  nan
                         use_std_format_locale(v):  nan
                             use_libfmt_locale(v):  nan
                                                v:  inf
                                std::to_string(v):  inf
                                   use_sprintf(v):  inf
              boost::lexical_cast<std::string>(v):  inf
                                      mimicked(v):  inf
                         use_std_format_locale(v):  inf
                             use_libfmt_locale(v):  inf
                                                v:  -inf
                                std::to_string(v):  -inf
                                   use_sprintf(v):  -inf
              boost::lexical_cast<std::string>(v):  -inf
                                      mimicked(v):  -inf
                         use_std_format_locale(v):  -inf
                             use_libfmt_locale(v):  -inf
                                                v:  0
                                std::to_string(v):  0.000000
                                   use_sprintf(v):  0.000000
              boost::lexical_cast<std::string>(v):  0
                                      mimicked(v):  0
                         use_std_format_locale(v):  0
                             use_libfmt_locale(v):  0
                                                v:  -0
                                std::to_string(v):  -0.000000
                                   use_sprintf(v):  -0.000000
              boost::lexical_cast<std::string>(v):  -0
                                      mimicked(v):  -0
                         use_std_format_locale(v):  -0
                             use_libfmt_locale(v):  -0
                                                v:  2,22045e-16
                                std::to_string(v):  0.000000
                                   use_sprintf(v):  0.000000
              boost::lexical_cast<std::string>(v):  2.2204460492503131e-16
                                      mimicked(v):  2.2204460492503131e-16
                         use_std_format_locale(v):  2.220446049250313e-16
                             use_libfmt_locale(v):  2.220446049250313e-16
                                                v:  3,14159
                                std::to_string(v):  3.141593
                                   use_sprintf(v):  3.141593
              boost::lexical_cast<std::string>(v):  3.1415926535897931
                                      mimicked(v):  3.1415926535897931
                         use_std_format_locale(v):  3.141592653589793
                             use_libfmt_locale(v):  3.141592653589793
    ==== override global locale
                                                v:  nan
                                std::to_string(v):  nan
                                   use_sprintf(v):  nan
              boost::lexical_cast<std::string>(v):  nan
                                      mimicked(v):  nan
                         use_std_format_locale(v):  nan
                             use_libfmt_locale(v):  nan
                                                v:  inf
                                std::to_string(v):  inf
                                   use_sprintf(v):  inf
              boost::lexical_cast<std::string>(v):  inf
                                      mimicked(v):  inf
                         use_std_format_locale(v):  inf
                             use_libfmt_locale(v):  inf
                                                v:  -inf
                                std::to_string(v):  -inf
                                   use_sprintf(v):  -inf
              boost::lexical_cast<std::string>(v):  -inf
                                      mimicked(v):  -inf
                         use_std_format_locale(v):  -inf
                             use_libfmt_locale(v):  -inf
                                                v:  0
                                std::to_string(v):  0,000000
                                   use_sprintf(v):  0,000000
              boost::lexical_cast<std::string>(v):  0
                                      mimicked(v):  0
                         use_std_format_locale(v):  0
                             use_libfmt_locale(v):  0
                                                v:  -0
                                std::to_string(v):  -0,000000
                                   use_sprintf(v):  -0,000000
              boost::lexical_cast<std::string>(v):  -0
                                      mimicked(v):  -0
                         use_std_format_locale(v):  -0
                             use_libfmt_locale(v):  -0
                                                v:  2,22045e-16
                                std::to_string(v):  0,000000
                                   use_sprintf(v):  0,000000
              boost::lexical_cast<std::string>(v):  2,2204460492503131e-16
                                      mimicked(v):  2,2204460492503131e-16
                         use_std_format_locale(v):  2,220446049250313e-16
                             use_libfmt_locale(v):  2,220446049250313e-16
                                                v:  3,14159
                                std::to_string(v):  3,141593
                                   use_sprintf(v):  3,141593
              boost::lexical_cast<std::string>(v):  3,1415926535897931
                                      mimicked(v):  3,1415926535897931
                         use_std_format_locale(v):  3,141592653589793
                             use_libfmt_locale(v):  3,141592653589793
    

    Note that mimicked and boost::lexical_cast<std::string>(double) result in exactly the same output each time.