stod некорректно работает с boost::locale

Я пытаюсь использовать boost::locale и std::stod вместе в немецкой локали, где запятая является десятичным разделителем. Рассмотрим этот код:

boost::locale::generator gen;

std::locale loc("");  // (1)
//std::locale  loc = gen("");  // (2)

std::locale::global(loc);
std::cout.imbue(loc);

std::string s = "1,1";  //float string in german locale!
double d1 = std::stod(s);
std::cout << "d1: " << d1 << std::endl;

double d2 = 2.2;
std::cout << "d2: " << d2 << std::endl;

std::locale loc("") создает правильную локаль и выводит

d1: 1,1
d2: 2,2

как я ожидаю. Когда я закомментирую строку (1) и раскомментирую строку (2), вывод будет

d1: 1
d2: 2.2

Результат для d2 ожидаем. Насколько я понимаю, boost::locale хочет, чтобы я явно указал, что d2 должен быть отформатирован как число, и

std::cout << "d2: " << boost::locale::as::number << d2 << std::endl;

снова фиксирует вывод на 2,2. Проблема в том, что std::stod больше не считает 1,1 действительным числом с плавающей запятой и усекает его до 1.

Мой вопрос: почему std::stod перестает работать, когда я генерирую свою локаль с помощью boost::locale?

Дополнительная информация: я использую VC++2015, Boost 1.60, без ICU, Windows 10.

Обновление:

Я заметил, что проблема устраняется, когда я дважды устанавливаю глобальную локаль, сначала с помощью std::locale(""), а затем с помощью boost:

std::locale::global(std::locale(""));
bl::generator gen;
std::locale::global(gen(""));

Я понятия не имею, почему он так себя ведет!


person NicolasR    schedule 12.01.2016    source источник
comment
это избавило бы вас от многих проблем в долгосрочной перспективе, если бы вы проверяли ошибки синтаксического анализа, предоставляя указатель idx на std::stod(str, idx)   -  person ead    schedule 13.01.2016
comment
Да, я делаю это в своем реальном коде, но удалил его для этого простого теста.   -  person NicolasR    schedule 13.01.2016
comment
Хороший! Я не знаю, насколько велик проект, но изменение глобальной локали может привести к поломке где-то еще, это должно быть последним средством... но когда вы используете std::stod, у вас нет выбора   -  person ead    schedule 13.01.2016
comment
Хорошая точка зрения. Но опять же, какие у меня есть варианты? Мой пользователь хочет, чтобы программное обеспечение считывало CSV-файлы с числами с плавающей запятой. Так что либо я использую локаль пользователя, либо ему придется явно указать десятичный разделитель. В последнем случае мне пришлось бы полностью отказаться от использования std::stod и тому подобного, потому что я, конечно, не хочу постоянно менять глобальную локаль. То, что казалось легким в начале, в конце оказывается кашей... :-(   -  person NicolasR    schedule 14.01.2016
comment
Если скорость не является самым важным требованием, вы можете использовать std::stringstream (посмотрите мое обновление). Другим быстрым вариантом будет boost::lexical_cast, но, если я правильно помню, это также зависит от глобального состояния (но не принимайте это за грандиозное)   -  person ead    schedule 14.01.2016


Ответы (1)


Короче говоря: boost::locale изменяет только глобальный объект локали C++, но не локаль C. stod использует C-locale, а не глобальный объект C++-locale. std::localeменяет оба: глобальный объект локали C++ и локаль C.


Вся история: std::locale — тонкая штука, на которую приходится много отладки!

Начнем с класса C++ std::locale:

  std::locale loc("de_DE.utf8");  
  std::cout<<loc.name()<<"\n\n\n";

создает немецкую локаль (если она доступна на вашей машине, иначе выкидывает), что приводит к de_DE.utf8 на консоли.

Однако он не меняет глобальный объект локали c++, который создается при запуске вашей программы и является классическим ("C"). Конструктор std::locale без аргументов возвращает копию глобального состояния:

...
  std::locale loc2;
  std::cout<<loc2.name()<<"\n\n\n";

Теперь вы должны увидеть C, если до этого ничего не испортило вашу локаль. std::locale("") сотворит магию и узнает предпочтения пользователя и вернет их как объект, без изменения глобального состояния.

Вы можете изменить локальное состояние с помощью std::local::global:

  std::locale::global(loc);
  std::locale loc3;
  std::cout<<loc3.name()<<"\n\n\n";

Конструктор по умолчанию на этот раз приводит к de_DE.utf8 на консоли. Мы можем восстановить глобальное состояние до классического, вызвав:

  std::locale::global(std::locale::classic());
  std::locale loc4;
  std::cout<<loc4.name()<<"\n\n\n";

что должно снова дать вам C.

Теперь, когда создается std::cout, он клонирует свою локаль из глобального состояния c++ (здесь мы делаем это со строковыми потоками, но это то же самое). Последующие изменения глобального состояния не влияют на поток:

 //classical formating
  std::stringstream c_stream;

 //german formating:
  std::locale::global(std::locale("de_DE.utf8"));
  std::stringstream de_stream;

  //same global locale, different results:
  c_stream<<1.1;
  de_stream<<1.1;

  std::cout<<c_stream.str()<<" vs. "<<de_stream.str()<<"\n";

Дает вам 1.1 vs. 1,1 - первое классическое второе немецкое

Вы можете изменить локальный объект потока с помощью imbue(std::locale::classic()) Само собой разумеется, что это не меняет глобальное состояние:

  de_stream.imbue(std::locale::classic());
  de_stream<<" vs. "<<1.1;
  std::cout<<de_stream.str()<<"\n";
  std::cout<<"global c++ state: "<<std::locale().name()<<"\n";

и вы видите:

1,1 vs. 1.1
global c++ state: de_DE.utf8

Теперь мы подходим к std::stod. Как вы можете себе представить, он использует глобальное состояние локали С++ (не совсем верно, потерпите меня), а не (частное) состояние cout-потока:

std::cout<<std::stod("1.1")<<" vs. "<<std::stod("1,1")<<"\n";

дает вам 1 vs. 1.1, потому что глобальное состояние по-прежнему "de_DE.utf8", поэтому первый синтаксический анализ останавливается на '.', но локальное состояние std::cout по-прежнему "C". После восстановления глобального состояния получаем классическое поведение:

  std::locale::global(std::locale::classic());
  std::cout<<std::stod("1.1")<<" vs. "<<std::stod("1,1")<<"\n";

Теперь немецкий "1,1" не анализируется должным образом: 1.1 vs. 1

Теперь вы можете подумать, что мы закончили, но есть еще кое-что - я обещал рассказать вам о std::stod.

Рядом с глобальной локалью C++ есть так называемая (глобальная) локаль C (происходит из языка C, и ее не следует путать с классической локалью "C"). Каждый раз, когда мы меняли глобальную локаль C++, локаль C тоже менялась.

Получить/установить локаль C можно с помощью std::setlocale(...). Чтобы запросить текущее значение, выполните:

std::cout<<"(global) C locale is "<<std::setlocale(LC_ALL,NULL)<<"\n";

чтобы увидеть (global) C locale is C. Чтобы установить локаль C, выполните:

  assert(std::setlocale(LC_ALL,"de_DE.utf8")!=NULL);
  std::cout<<"(global) C locale is "<<std::setlocale(LC_ALL,NULL)<<"\n";

что дает (global) C locale is de_DE.utf8. Но что теперь является глобальной локалью С++?

std::cout<<"global c++ state: "<<std::locale().name()<<"\n";

Как и следовало ожидать, C ничего не знает о глобальной локали C++ и оставляет ее неизменной: global c++ state: C.

Теперь мы больше не в Канзасе! Старые c-функции будут использовать C-локаль, а новая функция c++ — глобальный c++. Приготовьтесь к забавной отладке!

Что вы ожидаете

std::cout<<"C: "<<std::stod("1.1")<<" vs. DE :"<<std::stod("1,1")<<"\n";

сделать? std::stod — это совершенно новая функция С++ 11, и она должна использовать глобальную локаль С++! Подумайте еще...:

1 vs. 1.1

Он получает правильный немецкий формат, потому что C-locale установлен на 'de_DE.utf8', и он использует старые функции в стиле C под капотом.

Просто для полноты std::streams использует глобальную локаль С++:

  std::stringstream stream;//creating with global c++ locale
  stream<<1.1;
  std::cout<<"I'm still in 'C' format: "<<stream.str()<<"\n";

дает вам: I'm still in 'C' format: 1.1.

Редактировать. Альтернативный метод разбора строки без вмешательства в глобальную локаль и не отвлекаться на нее:

bool s2d(const std::string &str, double  &val, const std::locale &loc=std::locale::classic()){

  std::stringstream ss(str);
  ss.imbue(loc);
  ss>>val;
  return ss.eof() && //all characters interpreted
         !ss.fail(); //nothing went wrong
}

Следующие тесты показывают:

  double d=0;
  std::cout<<"1,1 parsed with German locale successfully :"<<s2d("1,1", d, std::locale("de_DE.utf8"))<<"\n";
  std::cout<<"value retrieved: "<<d<<"\n\n";

  d=0;
  std::cout<<"1,1 parsed with Classical locale successfully :"<<s2d("1,1", d, std::locale::classic())<<"\n";
  std::cout<<"value retrieved: "<<d<<"\n\n";

  d=0;
  std::cout<<"1.1 parsed with German locale successfully :"<<s2d("1.1", d, std::locale("de_DE.utf8"))<<"\n";
  std::cout<<"value retrieved: "<<d<<"\n\n";

  d=0;
  std::cout<<"1.1 parsed with Classical locale successfully :"<<s2d("1.1", d, std::locale::classic())<<"\n";
  std::cout<<"value retrieved: "<<d<<"\n\n";

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

1,1 parsed with German locale successfully :1
value retrieved: 1.1

1,1 parsed with Classical locale successfully :0
value retrieved: 1

1.1 parsed with German locale successfully :0
value retrieved: 11

1.1 parsed with Classical locale successfully :1
value retrieved: 1.1

std::stringstream может быть не самым быстрым, но имеет свои достоинства...

person ead    schedule 13.01.2016
comment
Спасибо за это прекрасное описание! Хотя это не совсем отвечает на мой вопрос, мне нужно было всего несколько шагов, чтобы найти его самостоятельно. Установка std::locale() в качестве глобальной локали успешно меняет локаль C, как вы описываете, но не на de_DE.utf8, а на German_Germany.1252 под Windows. Однако глобальная установка локали boost не меняет локаль C, и это меня не устраивает! Таким образом, единственное решение без явной ссылки на какое-либо имя локали, которое, кажется, отличается в системах Windows и * nix и которое я все равно не знаю заранее, - это установить локаль дважды. - person NicolasR; 13.01.2016