Каждый разработчик С++ рано или поздно сталкивается с особенностями конвертации дробного числа из строкового представления (std::string) в непосредственно число с плавающей точкой (float), связанными с установленной локалью (locale). Как правило, проблема возникает с различным представлением разделителя целой и дробной частей в десятичной записи числа ("," или ".").
В данной статье речь пойдет о двойственности локалей С++. Если Вам интересно, почему преобразование одной и той же std::string("0.1") с помощью std::stof() и std::istringstream во float может привести к различным результатам, прошу под кат.
Проблема
Как и во многих статьях Хабра, все началось с ошибки в коде, фрагмент которого можно свести к следующему:
float valf = std::stof(str); // где str = std::string("0.1")
std::cout << valf << std::endl; // печатает 0, а должен 0.1
«Дело в локали», — думаю я, поэтому в отладочных целях перед преобразованием дописываю строку вывода на экран действующего разделителя целой и дробной частей, ожидая увидеть там ",":
std::locale lcl; // создает копию текущей глобальной локали
const auto & facet = std::use_facet<std::numpunct<char>>(lcl);
std::cout << facet.decimal_point() << std::endl; // печатает точку!
std::locale lcl;
if (std::has_facet<std::numpunct<char>>(lcl))
{
//...
}
Подробнее про работу с фасетами и локалями в С++ можно узнать здесь: на Хабре, в документации.
Получается, что локаль установлена верная, и строка "0.1" должна преобразовываться корректно. Проверяем преобразование через std::istringstream:
float valf = std::stof(str); // где str = std::string("0.1")
std::cout << valf << std::endl; // печатает 0, а должен 0.1
std::istringstream iss(str);
iss >> valf;
std::cout << valf << std::endl; // печатает 0.1, все верно!
Получаем, что преобразование через std::istringstream работает как ожидается, в то время как std::stof() возвращает неверное значение.
Суть
В С++ существуют две глобальных локали:
- локаль STL, работа с которой возможна через фасеты и класс std::locale (#include <locale>);
- локаль С-библиотеки, работа с которой возможна с помощью функций setlocale() и localeconv() (#include <clocale>).
При этом смена глобальной локали с помощью функции std::locale::global() меняет как STL-локаль, так и локаль С-библиотеки, в то время как функция setlocale() влияет только на вторую.
Таким образом, возможно рассогласование:
auto * le = localeconv();
std::cout << le->decimal_point << std::endl; // печатает запятую
std::locale lcl; // создает копию текущей глобальной локали
const auto & facet = std::use_facet<std::numpunct<char>>(lcl);
std::cout << facet.decimal_point() << std::endl; // печатает точку!
Загвоздка заключается в том, что функция из C++11 std::stof() (как и std::stod()) базируется на функции strtod() (или wcstod()) из библиотеки С, которая, в свою очередь, ориентируется на локаль С-библиотеки. Получается, что поведение С++ функции опирается на локаль С-библиотеки, а не на локаль STL, как ожидается.
Заключение
Функции C++ STL в своей работе могут использовать функции С-библиотеки, что может приводить к неожиданному результату, в частности, в случае рассогласования глобальных локалей STL и С-библиотеки. Необходимо иметь это в виду.
В моем конкретном случае под *nix был «виноват» класс QCoreApplication библиотеки Qt, который при инициализации вызывает setlocale(), тем самым приводя к возможному рассогласованию описанных локалей.
P.S. Как многие верно подметят, библиотека Qt обладает своими средствами преобразования строки в число, как и своей собственной глобальной локалью (QLocale). Описанная ситуация возникла при интеграции кода из проекта, использующего только STL, в Qt-проект.
Автор: Александр