Несовместимый формат символов 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, "");, похоже, также приводит к сбою рабочих примеров, приведенных выше. Ввод «ΑΒΓ» становится «ΑΑΑ», и оба литерала являются бредом.

🤔 А знаете ли вы, что...
Язык C++ обеспечивает совместимость с языком C, что позволяет использовать код на обоих языках в одном проекте.


1
55
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.