Skip to content

Commit 7956a40

Browse files
author
matthias
committed
WT-9595: Rely on charconv for float/double precision for Postgres
The Postgres backend used to rely on `std::stof` and `std::stod`, which was replaced with `boost::spirit`. The latter, however, loses some precision. Hence we moved back to the original `std` functions, with the boost library being the fallback in case we encounter a `std::out_of_range` exception. Now, we use `std::to_chars` and `std::from_chars`, to parse and create strings from floats/doubles. This has the advantage that is does not rely on an external library, nor is it locale dependent. The caveat here is that this is C++17 (or C++14 as an extension). Again note some specific tests have certain cases for some backends. This is because MS SQL Server does not have the concepts of NaN or infinity. Nor is MariaDb able to hold NaN.
1 parent 5b53b3a commit 7956a40

File tree

2 files changed

+551
-2
lines changed

2 files changed

+551
-2
lines changed

src/Wt/Dbo/backend/Postgres.C

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@
2929
#include <cstring>
3030
#include <ctime>
3131

32+
/* While <charconv> is part of C++17, it can be used by earlier versions as an extension.
33+
* This entails that it is possible some implementations lack features. Hence the
34+
* #ifdef __cpp_lib_to_chars additional check.
35+
*/
36+
#if defined __has_include
37+
# if __has_include (<charconv>)
38+
# include <charconv>
39+
# ifdef __cpp_lib_to_chars
40+
# define WT_CPP_LIB_TO_CHARS
41+
# endif
42+
# endif
43+
#endif
44+
3245
#include <boost/config/warning_disable.hpp>
3346
#include <boost/spirit/include/karma.hpp>
3447
#include <boost/spirit/include/qi_parse.hpp>
@@ -106,6 +119,17 @@ namespace {
106119

107120
static inline std::string double_to_s(const double d)
108121
{
122+
#ifdef WT_CPP_LIB_TO_CHARS
123+
std::array<char, 30> buf;
124+
auto returnValue = std::to_chars(buf.begin(), buf.end(), d);
125+
126+
if (returnValue.ec != std::errc()) {
127+
throw std::invalid_argument(
128+
"double_to_s: to_chars failed for double (as to_string() '" +
129+
std::to_string(d) + "'), err: " + std::error_condition(returnValue.ec).message());
130+
}
131+
return std::string(buf.data(), returnValue.ptr - buf.data());
132+
#else
109133
char buf[30];
110134
char *p = buf;
111135
if (d != 0) {
@@ -115,10 +139,22 @@ namespace {
115139
}
116140
*p = '\0';
117141
return std::string(buf, p);
142+
#endif
118143
}
119144

120145
static inline std::string float_to_s(const float f)
121146
{
147+
#ifdef WT_CPP_LIB_TO_CHARS
148+
std::array<char, 30> buf;
149+
auto returnValue = std::to_chars(buf.begin(), buf.end(), f);
150+
151+
if (returnValue.ec != std::errc()) {
152+
throw std::invalid_argument(
153+
"float_to_s: to_chars failed for float (as to_string() '" +
154+
std::to_string(f) + "'), err: " + std::error_condition(returnValue.ec).message());
155+
}
156+
return std::string(buf.data(), returnValue.ptr - buf.data());
157+
#else
122158
char buf[30];
123159
char *p = buf;
124160
if (f != 0) {
@@ -128,6 +164,7 @@ namespace {
128164
}
129165
*p = '\0';
130166
return std::string(buf, p);
167+
#endif
131168
}
132169
}
133170

@@ -553,12 +590,31 @@ public:
553590
if (PQgetisnull(result_, row_, column))
554591
return false;
555592

593+
594+
#ifdef WT_CPP_LIB_TO_CHAR
595+
std::string result_s = PQgetvalue(result_, row_, column);
596+
597+
// try to convert with from_chars which has good round-trip properties
598+
auto returnValue = std::from_chars(result_s.data(), result_s.data() + result_s.size(), *value);
599+
600+
// fall-back to boost::spirit for "out of range", e.g. subnormals in some implementations
601+
if (returnValue.ec == std::errc::result_out_of_range) {
602+
try {
603+
*value = std::stof(PQgetvalue(result_, row_, column));
604+
} catch (std::out_of_range&) {
605+
*value = convert<float>("stof", boost::spirit::float_, PQgetvalue(result_, row_, column));
606+
}
607+
} else if (returnValue.ec != std::errc()) {
608+
throw PostgresException(std::string("getResult: from_chars (float) of '") +
609+
result_s + "' failed, err: " + std::error_condition(returnValue.ec).message());
610+
}
611+
#else
556612
try {
557613
*value = std::stof(PQgetvalue(result_, row_, column));
558614
} catch (std::out_of_range&) {
559615
*value = convert<float>("stof", boost::spirit::float_, PQgetvalue(result_, row_, column));
560616
}
561-
617+
#endif
562618
LOG_DEBUG(this << " result float " << column << " " << *value);
563619

564620
return true;
@@ -569,12 +625,31 @@ public:
569625
if (PQgetisnull(result_, row_, column))
570626
return false;
571627

628+
629+
#ifdef WT_CPP_LIB_TO_CHAR
630+
std::string result_s = PQgetvalue(result_, row_, column);
631+
632+
// try to convert with from_chars which has good round-trip properties
633+
auto returnValue = std::from_chars(result_s.data(), result_s.data() + result_s.size(), *value);
634+
635+
// fall-back to boost::spirit for "out of range", e.g. subnormals in some implementations
636+
if (retutnValue.ec == std::errc::result_out_of_range) {
637+
try {
638+
*value = std::stod(PQgetvalue(result_, row_, column));
639+
} catch (std::out_of_range&) {
640+
*value = convert<double>("stod", boost::spirit::double_, PQgetvalue(result_, row_, column));
641+
}
642+
} else if (returnValue.ec != std::errc()) {
643+
throw PostgresException(std::string("getResult: from_chars (float) of '") +
644+
result_s + "' failed, err: " + std::error_condition(returnValue.ec).message());
645+
}
646+
#else
572647
try {
573648
*value = std::stod(PQgetvalue(result_, row_, column));
574649
} catch (std::out_of_range&) {
575650
*value = convert<double>("stod", boost::spirit::double_, PQgetvalue(result_, row_, column));
576651
}
577-
652+
#endif
578653
LOG_DEBUG(this << " result double " << column << " " << *value);
579654

580655
return true;

0 commit comments

Comments
 (0)