Несовместимый формат символов UTF-8 в C++

Я пытался разработать небольшое консольное приложение на C++ для Windows, которое взаимодействует с базой данных SQLite. Однако эта база данных может содержать символы UTF-8, например. Греческие буквы. Поэтому программе необходимо вводить эти символы из пользовательской консоли, использовать их в запросах и выводить.

Я хотел бы ввести эти символы, используя getline или, в идеале, getch.

Сначала даже простой ввод и вывод строки utf-8 не работал.

С использованием

    SetConsoleOutputCP(CP_UTF8);
    SetConsoleCP(CP_UTF8);

Введенные строки имели правильную длину, но все символы были нулевыми. Например. ввод «ΑΒΓ» сохранит строку из 3 символов со значением 0.

Использование 1253 (кодовая страница для греческого языка) вместо CP_UTF8 работало для объединения, ввода и вывода. В отладчике я заметил, что значения строк недействительны и отображаются неправильно. Я также заметил, что вместо 2 байтов на символ был только 1, но, поскольку выводился нормально, я не особо задумывался об этом, поскольку потери данных быть не могло.

Однако API SQLite с этим не согласился. Построение запроса с использованием пользовательского ввода не даст результата. Выполнение запроса с использованием жестко закодированного строкового литерала с тем же символом utf-8 дало бы результат, но и жестко закодированный запрос, и результаты имели другой формат, чем пользовательский ввод, который я получал раньше. Они правильно отображались в отладчике, имели по 2 байта на символ и выводили тарабарщину под 1253 CP, но правильно под CP_UTF8.

Я не нашел никаких упоминаний об этом несоответствии в Интернете, хотя не уверен, что вообще смотрел в нужных местах. Поскольку результаты SQLite выводятся правильно под CP_UTF8, мне бы хотелось, по крайней мере, иметь возможность вводить символы в желаемом формате API SQLite (т. е. в том же формате, в котором хранятся литералы) или, по крайней мере, иметь возможность конвертировать из текущего 1253 формат к нему.

Ниже приведен минимальный воспроизводимый пример с концепциями, упомянутыми выше:

#include <iostream>
#include<string>
#include<windows.h>

using namespace std;

int main()

{

    SetConsoleOutputCP(CP_UTF8);
    SetConsoleCP(CP_UTF8);

    //Dysfunctional Input
    string input1;
    cout << "Enter greek letters: ";
    cin >> input1;
    cout << "You entered: " << input1 << endl; //input1 has correct size but all chars are null

    SetConsoleOutputCP(1253);
    SetConsoleCP(1253);

    //Working Input and Output
    string input2;
    cout << "Enter greek letters: ";
    cin >> input2;
    cout << "You entered: " << input2 << endl; //Should output properly
    
    //Printing a literal
    string literall = u8"Γειά σας";
    cout << "Literal under 1253: " << literall << endl; //Giberish

    //Printing a literal under CP_UTF8
    SetConsoleOutputCP(CP_UTF8);
    cout << "Literal under CP_UTF8: " << literall << endl; //Correct output

    return 0;
}

Приведенный выше код показывает аналогичные результаты с getline и _getch вместо cin, что многообещающе.

Заключительные замечания:

  • Терминал, похоже, не является проблемой, поскольку я могу вводить и выводить греческие буквы, когда установлен правильный ConsoleCP.
  • setlocale(LC_ALL, "");, похоже, также приводит к сбою рабочих примеров, приведенных выше. Ввод «ΑΒΓ» становится «ΑΑΑ», и оба литерала являются бредом.

Весь смысл кодирования заключается в том, чтобы одни и те же байты представляли совершенно разные символы. Попытка вывести UTF-8 с использованием кодировки 1253 почти по определению приведет к тарабарщине.

Mark Ransom 21.08.2024 00:12

Вы уверены, что ваш SQLite настроен на использование UTF-8?

Mark Ransom 21.08.2024 00:12

А 2 байта на символ подразумевают UTF-16 вместо UTF-8.

Mark Ransom 21.08.2024 00:13

@MarkRansom, база данных содержала записи с данными на греческом языке, а запросы жесткого кодирования в моем консольном приложении дают правильный результат, как упоминалось выше. Я предполагаю, что это означает, что он настроен на использование UTF-8?

andy dexter 21.08.2024 00:19

Литерал, определенный в приведенном выше примере, имеет длину 8 букв, но в отладчике отображается как размер 15. Означает ли это, что он хранится в UTF-16? API C-SQLite поддерживает только UTF-8, но его результаты имеют по 2 байта на греческий символ, поэтому я не уверен, как это понимать.

andy dexter 21.08.2024 00:24

Вы компилируете с флагом компилятора /utf-8 ?

Eljay 21.08.2024 00:44

@Элджей, я только что добавил это, и, похоже, это не имеет значения. Оказывает ли это какое-либо влияние на наборы символов консоли?

andy dexter 21.08.2024 01:00

@andydexter флаг /utf-8 вообще не влияет на консоль, а только на набор символов, который компилятор использует для анализа вашего исходного кода и для хранения узких строковых литералов в исполняемом файле. В этом случае вам лучше НЕ пытаться настроить консоль на использование UTF-8. Вместо этого используйте std::wcin и std::wstring для работы со строками UTF-16 (поскольку Windows является собственной ОС UTF-16) и преобразуйте данные в UTF-8 при передаче их в API SQLite, используя std::wstring_convert или WideCharToMultiByte(), ICONV/ICU или любой другой другой эквивалентный API/библиотека обработки Unicode по вашему выбору.

Remy Lebeau 21.08.2024 01:20

Хорошо, 15 байт, а не 16. Это не «два байта на символ». Почти так и есть, потому что, за исключением пробела, эти греческие символы в UTF-8 превращаются в два байта каждый, но это просто совпадение. Конкретно это "\xce\x93\xce\xb5\xce\xb9\xce\xac \xcf\x83\xce\xb1\xcf\x82". UTF-8 может содержать от 1 до 4 байтов на символ, в зависимости от символа.

Mark Ransom 21.08.2024 01:55
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
9
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

UTF-8 и консоль Windows

Мне никогда не удавалось заставить консоль Windows работать напрямую с UTF-8, не запутываясь ни в самой Windows, ни в компиляторе/версии/libc/и т. д.

Однако использование консольного API с функциями расширенных символов всегда работает. Таким образом, чтобы обеспечить работу ввода-вывода UTF-8, вам необходимо соответствующим образом наполнить стандартные потоки фильтрами преобразования. Следующие настройки настраивают ситуацию:

#include <windows.h>
#include <shellapi.h>

#pragma comment(lib, "Shell32")

///////////////////////////////////////////////////////////////////////////////////////////////////
namespace duthomhas::utf8::console
///////////////////////////////////////////////////////////////////////////////////////////////////
{

//-------------------------------------------------------------------------------------------------
struct Input: public std::streambuf
//-------------------------------------------------------------------------------------------------
{
  using int_type = std::streambuf::int_type;
  using traits   = std::streambuf::traits_type;

  HANDLE  handle;
  char    buffer[ 4 ];
  wchar_t c;
  DWORD   n;

  Input( HANDLE handle ): handle(handle) { }
  Input( const Input& that ): handle(that.handle) { }

  virtual int_type underflow() override
  {
    auto ok = ReadConsoleW( handle, &c, 1, &n, NULL );
    if (!ok or !n) return traits::eof();
    if (c == '\r') return underflow();

    n = WideCharToMultiByte( CP_UTF8, 0, (const wchar_t*)&c, 1, (char*)buffer, sizeof( buffer ), NULL, NULL );
    setg( buffer, buffer, buffer + n );

    return n ? traits::to_int_type( *buffer ) : traits::eof();
  }
};

//-------------------------------------------------------------------------------------------------
struct Output: public std::streambuf
//-------------------------------------------------------------------------------------------------
{
  using int_type = std::streambuf::int_type;
  using traits   = std::streambuf::traits_type;

  HANDLE      handle;
  std::string buffer;

  Output( HANDLE handle ): handle(handle) { }
  Output( const Output& that ): handle(that.handle) { }

  virtual int_type sync() override
  {
    DWORD n;
    std::wstring s( buffer.size(), 0 );
    s.resize( MultiByteToWideChar( CP_UTF8, 0, (char*)buffer.c_str(), (int)buffer.size(), (wchar_t*)s.c_str(), (int)s.size() ) );
    if (buffer.size() and s.empty()) return -1;
    buffer.clear();
    return WriteConsoleW( handle, (wchar_t*)s.c_str(), (DWORD)s.size(), &n, NULL ) ? 0 : -1;
  }

  virtual int_type overflow( int_type value ) override
  {
    buffer.push_back( traits::to_char_type( value ) );
    if (traits::to_char_type( value ) == '\n') sync();
    return value;
  }
};

//-------------------------------------------------------------------------------------------------
void initialize()
//-------------------------------------------------------------------------------------------------
{
  // Update the standard I/O streams, maybe
  DWORD mode; HANDLE
  handle = GetStdHandle( STD_INPUT_HANDLE  ); if (GetConsoleMode( handle, &mode )) std::cin .rdbuf( new Input ( handle ) );
  handle = GetStdHandle( STD_OUTPUT_HANDLE ); if (GetConsoleMode( handle, &mode )) std::cout.rdbuf( new Output( handle ) );
  handle = GetStdHandle( STD_ERROR_HANDLE  ); if (GetConsoleMode( handle, &mode )) std::cerr.rdbuf( new Output( handle ) );
}

} // namespace duthomhas::utf8::console

Теперь в вашем main() обязательно инициализируйте:

int main(...)
{
  duthomhas::utf8::console::initialize();

  // Ask the user to "Enter some Greek"
  std::cout << "Βάλε λίγα ελληνικά: ";
  std::string s;
  getline( std::cin, s );
  std::cout << "Good job! You entered: " << s << "!\n";

Опять же, это всегда работает — потому что он обходит обычную обработку «символ — это байт» и использует обработку Windows UTF-16 непосредственно под капотом — но только если вы действительно подключены к консоли!

⟶ Do remember, though, that the Windows console cannot handle anything outside the BMP. Redirected file I/O still works with the full Unicode set.

Кодовая точка Юникода ≠ один символ ≠ один байт

Полный код Юникода имеет длину 21 бит и обычно хранится в 32-битном целочисленном объекте (например, char32_t).

Если вы хотите обрабатывать UTF-8, вы больше не можете рассматривать «символы» как байтовые значения. Один символ может иметь длину от одного до четырех байтов, а каждый последующий символ может иметь разное количество байтов.

Кроме того, один «символьный глиф» может состоять из более чем одной кодовой точки!

tl;dr: все представляет собой строку.

Почти все, что вы захотите сделать с помощью UTF-8, можно обработать как подстроки, и вам следует структурировать свой код соответствующим образом.

Если вы планируете что-либо делать с данными UTF-8, вам следует взглянуть на ICU.
Вот еще один ответ, который я написал конкретно об использовании отделения интенсивной терапии.

⟶ ICU also includes functions for interacting with the console, but they are not out-of-the-box-supported on Windows — again due to compiler/version/etc.

Большое спасибо за ответ, все отлично работает из коробки. К счастью, мне не нужно ничего делать с этими строками, кроме отображения. Можно ли примерно объяснить, что переопределяет ваше решение?

andy dexter 21.08.2024 07:15

Также есть ли способ использовать аналогичную реализацию для анализа символов с использованием _getch()? Я понимаю, что он возвращает только байт ASCII символа, но после некоторого тестирования я заметил, что при вводе греческих символов 2 байта буферизуются, поэтому вы можете использовать 2 _getch(), чтобы получить один символ, хотя я думаю, что нет никакого способа узнать, как это сделать. длинный каждый символ.

andy dexter 21.08.2024 07:47

Буферы потоков ввода и вывода переопределяются для использования функций ввода-вывода всей консоли Win32 вместо методов ввода-вывода по умолчанию (какими бы они ни были). Такие вещи, как _getch(), предназначены для нажатия одной клавиши, что совершенно отличается от «символа».

Dúthomhas 21.08.2024 07:58

Хорошей новостью является то, что ввод с клавиатуры имеет тенденцию осуществляться частями, поэтому, если пользователь вводит символ, вы, безусловно, можете проверить, доступен ли какой-либо ввод, и прочитать весь ввод сразу, и он будет представлять собой один «символ». Но опять же, это работает только тогда, когда вы подключены к клавиатуре. Возможно, стоит задать новый вопрос именно по этому поводу. (Однако не знаю, смогу ли я дать вам полезный ответ сейчас.)

Dúthomhas 21.08.2024 07:59

Понятно, но я предполагаю, что эти куски не разделены ничем, кроме времени, верно? Поскольку _getch() и все другие известные мне методы ввода блокируются, я не знаю, как проверить, доступен ли ввод. Если понадобится, я рассмотрю возможность задать новый вопрос позже. В любом случае спасибо!

andy dexter 21.08.2024 08:58

Время соответствует скорости, с которой человек может изменить положение пальца, чтобы нажать другую клавишу. Компьютер работает намного быстрее.

Dúthomhas 21.08.2024 09:06

Другие вопросы по теме