lexical_cast Boost От двойной до строковой точности

Я работаю с библиотекой, которая, к сожалению, использует boost::lexical_cast для преобразования из double в string.

Мне нужно иметь возможность окончательно отразить это поведение на моей стороне, но я прыгал, чтобы сделать это, не распространяя boost.

Можно ли гарантировать идентичное поведение при использовании to_string, sprintf или какой-либо другой функции, содержащейся в стандарте?


person Jonathan Mee    schedule 03.01.2018    source источник
comment
Почему "неудачно"?   -  person SergeyA    schedule 03.01.2018
comment
@SergeyA К сожалению, я не могу найти никакой документации о точном поведении boost::lexical_cast и потому что я не хочу еще больше загромождать, продолжая втягивать boost.   -  person Jonathan Mee    schedule 03.01.2018
comment
В чем именно проблема с использованием boost? Очевидно, проект уже использует его? По-видимому, лексический состав boost не привязан к определенному способу представления строк и поэтому может меняться в разных версиях.   -  person SergeyA    schedule 03.01.2018
comment
@SergeyA Одна из библиотек, которые я использую в Boost. (Очевидно, я могу заглянуть в исходный код этой библиотеки, поскольку знаю, что она использует boost::lexical_cast для выполнения этого преобразования.) У меня есть возможность оптимизации для выполнения этого преобразования double в string на моей стороне, но результирующее string должно соответствовать string, которое библиотека сгенерировала бы.   -  person Jonathan Mee    schedule 03.01.2018
comment
(В зависимости от характера библиотеки, возможно, вам удастся отформатировать числа в строку, прежде чем делиться ими. Конечно, во многих случаях это невозможно)   -  person sehe    schedule 04.01.2018


Ответы (2)


Буст-код заканчивается здесь:

            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;
            }

Вам повезло, так как точность ОБЫЧНО постоянна во время компиляции (если только boost не настраивает BOOST_LCAST_NO_COMPILE_TIME_PRECISION).

Немного упрощая и позволяя использовать современные стандартные библиотеки:

Имитация 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;
    }
}

Регрессионные тесты

Чтобы сравнить результаты и проверить предположения:

Жить на Coliru

#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 <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;
}

void tests() {
    for (double v : {
            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(),
            M_PI })
    {
        TEST(v);
        TEST(std::to_string(v));
        TEST(use_sprintf(v));
        TEST(boost::lexical_cast<std::string>(v));
        TEST(mimicked(v));

        assert(mimicked(v) == boost::lexical_cast<std::string>(v));
    }
}

static std::locale DE("de_DE.utf8");

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();
}

Отпечатки

                                        v:  nan
                        std::to_string(v):  nan
                           use_sprintf(v):  nan
      boost::lexical_cast<std::string>(v):  nan
                              mimicked(v):  nan
                                        v:  inf
                        std::to_string(v):  inf
                           use_sprintf(v):  inf
      boost::lexical_cast<std::string>(v):  inf
                              mimicked(v):  inf
                                        v:  -inf
                        std::to_string(v):  -inf
                           use_sprintf(v):  -inf
      boost::lexical_cast<std::string>(v):  -inf
                              mimicked(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
                                        v:  -0
                        std::to_string(v):  -0.000000
                           use_sprintf(v):  -0.000000
      boost::lexical_cast<std::string>(v):  -0
                              mimicked(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
                                        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
==== imbue std::cout
                                        v:  nan
                        std::to_string(v):  nan
                           use_sprintf(v):  nan
      boost::lexical_cast<std::string>(v):  nan
                              mimicked(v):  nan
                                        v:  inf
                        std::to_string(v):  inf
                           use_sprintf(v):  inf
      boost::lexical_cast<std::string>(v):  inf
                              mimicked(v):  inf
                                        v:  -inf
                        std::to_string(v):  -inf
                           use_sprintf(v):  -inf
      boost::lexical_cast<std::string>(v):  -inf
                              mimicked(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
                                        v:  -0
                        std::to_string(v):  -0.000000
                           use_sprintf(v):  -0.000000
      boost::lexical_cast<std::string>(v):  -0
                              mimicked(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
                                        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
==== override global locale
                                        v:  nan
                        std::to_string(v):  nan
                           use_sprintf(v):  nan
      boost::lexical_cast<std::string>(v):  nan
                              mimicked(v):  nan
                                        v:  inf
                        std::to_string(v):  inf
                           use_sprintf(v):  inf
      boost::lexical_cast<std::string>(v):  inf
                              mimicked(v):  inf
                                        v:  -inf
                        std::to_string(v):  -inf
                           use_sprintf(v):  -inf
      boost::lexical_cast<std::string>(v):  -inf
                              mimicked(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
                                        v:  -0
                        std::to_string(v):  -0,000000
                           use_sprintf(v):  -0,000000
      boost::lexical_cast<std::string>(v):  -0
                              mimicked(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
                                        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

Обратите внимание, что mimicked и boost::lexical_cast<std::string>(double) каждый раз дают один и тот же результат.

person sehe    schedule 03.01.2018
comment
В интересах полного излишества добавлены тестовые примеры, включая NaN, +Inf, -Inf, +0, -0, эпсилон и M_PI, которые asserts, что вывод mimicked(v) всегда идентичен boost::lexical_cast<std::string>(v) при всех комбинациях локалей. - person sehe; 04.01.2018
comment
Тьфу, и мне потребовалось так много времени, чтобы понять первое, что ты выучил ›:( - person Jonathan Mee; 04.01.2018
comment
Я сделал только предположение о точности времени компиляции. Мне нравится моя версия, так как она работает в ‹30 LoC и будет работать точно так же на всех платформах и для всех реальных типов. - person sehe; 04.01.2018
comment
Я принял ваш ответ, потому что он фантастический, и я хотел бы дать ему более одного голоса. Но только для моего собственного понимания, что означает ‹30 LoC? - person Jonathan Mee; 04.01.2018
comment
Итак, менее 30 строк кода (теперь, когда я дома за настоящей клавиатурой). Ваше здоровье! - person sehe; 05.01.2018

Итак, после нескольких часов копания в шаблонах Boost вот что я узнал:

  1. Фактический вызов, выполняющий преобразование строк: lexical_cast_do_cast<std::string, double>::lexical_cast_impl
  2. Это использует std::sprintf в пределах boost::detail::lexical_stream_limited_src<char, std::char_traits<char>, false>
  3. boost::detail::lexical_stream_limited_src<char, std::char_traits<char>, true>::operator<< будет использоваться для вставки double, передачи begin, указателя на выделенный буфер std::string, и val, вход double, дает этот вызов: std::sprintf(begin, "%.*g", static_cast<int>(boost::detail::lcast_get_precision<double>()), val)
  4. Таким образом, поле точности здесь происходит от boost::details::lcast_precision<double>::value, которое будет использовать std::numeric_limits<double>; если is_specialized равно false, is_exact равно false, radix равно 2, а digits больше 0, то boost::details::lcast_precision<double>::value будет оцениваться как: 2UL + std::numeric_limits<double>::digits * 30103UL / 100000UL

Таким образом, где begin — это выделенное string, а val — входное значение типа double, boost::lexical_cast<double> дает окончательный результат, эквивалентный следующему:

std::sprintf(begin, "%.*g", 2UL + std::numeric_limits<double>::digits * 30103UL / 100000UL, val)

Это, очевидно, сильно зависит от реализации. Но в моей системе это даст точный эквивалент.

person Jonathan Mee    schedule 03.01.2018
comment
Извлеченные уроки: никогда не используйте Boost, всегда используйте стандартную функциональность C++. - person Jonathan Mee; 04.01.2018
comment
Да, это хороший анализ. Проголосуйте. и да, я удалю свой ответ. Хотя я верю, что у Boost есть свое применение: дух превосходен, как и BLAS, и библиотеки дат. Но да, принимайте то, что указано в стандарте. - person Bathsheba; 04.01.2018
comment
Мое мнение: всегда используйте Boost, но не относитесь к нему как к серебряной пуле. Если вам нужно знать, что вы делаете, убедитесь, что вы знаете, как вы это делаете. Так всегда: компании никогда не должны отдавать на аутсорсинг свой основной бизнес, например. - person sehe; 04.01.2018
comment
@Bathsheba С сожалением должен сказать, что я согласен с тем, что дух является исключением, и, видимо, мне нужно прочитать о том, что такое BLAS. Что касается дат, я настоятельно рекомендую библиотеку Говарда Хиннанта, которая была предложена для добавления к стандарту: open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0355r4.html - person Jonathan Mee; 04.01.2018