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?
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:
#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;
}
}
To compare the results and check the assumptions:
#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.