Search code examples
c++boost

Match round behavior of std::stringstream with boost::spirit::karma::real_generator


I want to match the output of boost::spirit::karma::real_generator to the output of std::stringstream.

The following code converts the double 8612.0078125 to std::string with precision 6 using:

  1. stringstream and std::setprecision
  2. boost::spirit::karma::real_generator

However, they behave differently in the last digit. The output:

8612.007812
8612.007813

I checked karma::real_policies if there was a policy for rounding behavior, but n is already 7813 when fraction_part is called.

#include <boost/spirit/include/karma.hpp>

#include <iomanip>

template <typename T>
struct precision_policy : boost::spirit::karma::real_policies<T>
{
  int floatfield(T n) const { return boost::spirit::karma::real_policies<T>::fmtflags::fixed; } // Always Fixed
  bool trailing_zeros(T n) const{ return true; }

  precision_policy(int prec):precision_(prec){}
  int precision(T n) const { return precision_; }
  int precision_;
  
};
// https://stackoverflow.com/questions/42255919/using-boost-karma-to-replace-stdstringstream-for-double-to-stdstring-convers
std::string ToStringFixedKarma(double d, const unsigned int width = 6)
{
  using boost::spirit::karma::real_generator;
  using boost::spirit::ascii::space;
  using boost::spirit::karma::generate;

  real_generator<double,precision_policy<double> > my_double_(width);

  std::string s;
  std::back_insert_iterator<std::string> sink(s);
  generate(sink, my_double_, d);
  return s;
}

int main(){

  double my_double = 8612.0078125;

  std::stringstream description;
  description << std::fixed << std::setprecision(6)
              << my_double << "\n";
              
  std::cout << description.str();
  
  std::cout << ToStringFixedKarma(my_double) << "\n";

}

Solution

  • I hate to be that guy, but this strongly feels like a problem of square pegs and round holes.

    Karma has its use, but in isolation to format reals, I would not consider it inevitable.

    The linked answer suggests that you might be after the performance boost. I suspect that this can also be had by using a stream on the same std::string instance repeatedly. Here is a simple take that - obviously - replicates the iostream behaviour:

    std::string ToStringFixedNoKarma(double d, const unsigned int width = 6) {
        using D = boost::iostreams::back_insert_device<std::string>;
        std::string s;
        boost::iostreams::stream<D> ss(D{s});
        ss << std::fixed << std::setprecision(width) << d;
        return s;
    }
    

    Live On Coliru

    You can expect it to be faster due to the ability to move from the string result and/or to reuse the backing string instance.


    Other ideas/source of inspiration:

    • Boost Convert has an array of conversion methods, including benchmarks

      enter image description here

      Also note the remark about Karma performance.

    • If you're after correctness and speed, consider Libfmt:

      • Fast IEEE 754 floating-point formatter with correct rounding, shortness and round-trip guarantees

      They too publish benchmarks:

      enter image description here

    • C++17 has the new to_chars in <charconv>. Sadly GCC doesn't implement it fully yet (but MSVC does).

    In an attempt to be helpful a bit here's a side-by-side of many of the approaches mentioned:

    Live On Compiler Explorer

    #include <boost/convert.hpp>
    #include <boost/convert/lexical_cast.hpp>
    #include <boost/convert/parameters.hpp>
    #include <boost/convert/printf.hpp>
    #include <boost/convert/stream.hpp>
    #include <boost/convert/strtol.hpp>
    #include <boost/iostreams/device/back_inserter.hpp>
    #include <boost/iostreams/stream.hpp>
    #include <boost/multiprecision/cpp_dec_float.hpp>
    #include <fmt/printf.h>
    #include <charconv>
    
    #include <iostream>
    using Reference = boost::multiprecision::cpp_dec_float_100;
    
    std::string ToStringFixedKarma(double d, const unsigned int width = 6) {
        using D = boost::iostreams::back_insert_device<std::string>;
        std::string s;
        boost::iostreams::stream<D> ss(D{s});
        ss << std::fixed << std::setprecision(width) << d;
        return s;
    }
    
    void comparisons(std::string_view label, Reference value, auto converter) {
        double d = value.convert_to<double>();
        float  f = value.convert_to<float>();
    
        std::cout << " ---- " << label << "\n";
        std::cout << "float"  << converter(f) << "\n";
        std::cout << "double" << converter(d) << "\n";
    }
    
    int main() {
        namespace cnv = boost::cnv;
        namespace arg = boost::cnv::parameter;
        cnv::cstream      _cs;
        cnv::lexical_cast _lc; // not able to control format
        cnv::strtol       _stl;
        cnv::printf       _pf;
    
        Reference const reference("8612.0078125");
        std::cout << "Reference: " << reference.str(20, std::ios::fixed) << "\n";
    
        _cs(std::fixed)(std::setprecision(6));
        _stl(arg::notation = cnv::notation::fixed)(arg::precision = 6);
        _pf(arg::notation = cnv::notation::fixed)(arg::precision = 6);
    
        auto cs  = cnv::apply<std::string>(boost::cref(_cs));
        auto stl = cnv::apply<std::string>(boost::cref(_stl));
        auto pf  = cnv::apply<std::string>(boost::cref(_pf));
        auto lc  = cnv::apply<std::string>(boost::cref(_lc));
    
        comparisons("cstream", reference, cs);
        comparisons("strtol", reference, stl);
        comparisons("printf", reference, pf);
        comparisons("libfmt", reference, [](auto v) { return fmt::format(FMT_STRING("{:.10}"), v); });
    #ifdef __cpp_lib_to_chars
        comparisons("charconv", reference,
                    [buf = std::array<char, 30>{}](auto v) mutable {
                        auto r = std::to_chars(buf.data(), buf.data() + buf.size(),
                                            v, std::chars_format::fixed, 6);
                        return std::string_view(buf.data(), r.ptr - buf.begin());
                    });
    #endif
        comparisons("lexical_cast", reference, lc);
    }
    

    Prints

    Reference: 8612.00781250000000000000
     ---- cstream
    float8612.007812
    double8612.007812
     ---- strtol
    float8612.007813
    double8612.007813
     ---- printf
    float8612.007812
    double8612.007812
     ---- libfmt
    float8612.007812
    double8612.007812
     ---- lexical_cast
    float8612.00781
    double8612.0078125