Я пытался разработать небольшое консольное приложение на 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
, что многообещающе.
Заключительные замечания:
setlocale(LC_ALL, "");
, похоже, также приводит к сбою рабочих примеров, приведенных выше. Ввод «ΑΒΓ» становится «ΑΑΑ», и оба литерала являются бредом.🤔 А знаете ли вы, что...
Язык C++ обеспечивает совместимость с языком C, что позволяет использовать код на обоих языках в одном проекте.
Мне никогда не удавалось заставить консоль 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.