你所热爱的,就是你的 Windows。

茶 栗

我想要解决方案,gkd

我把这个问题的解决方案和上一篇文章(讲 UTF-8 文本在命令行里对齐的方案)的方案整理到了一个 GitHub Repo 中,可以方便的引用到你的 CMake 项目中(如果不想,也可以手动把 src 和 include 复制到你的项目中,配置好编译)。同时为了说明这一方案的优雅性,我把这个 repo 里给的例子也贴在这里:

#include <iostream>
#include <iomanip>
#include <string>

#include "utf8_console.h"
using namespace std;

int main()
{
    initialize_utf8_console();

    string test_str1 = "测试 アイウエオ 😅🤣";
    string test_str2 = "テスト Россия";
    cout << "Test UTF-8 aligned output: " << endl
         << right << setw_u8(30, test_str1) << test_str1 << endl
         << right << setw_u8(30, test_str2) << test_str2 << endl
         << endl;

    cout << "Test UTF-8 input: Use EOF as end." << endl;
    string str;
    while (getline(cin, str))
        cout << str << endl;
    return 0;
}
Demo Picture
运行效果 (Windows Terminal 下)

细节

还是同一个高程课,有同学在群里问怎么用 cin 读 UTF-8。我以为就 SetConsoleCP 之后就可以用 cin 嘎嘎读,结果自己试了试,并没有那么简单。

直到2022年,微软还没有把Windows上的UTF-8字符处理弄明白。我尝试了以下操作,试图从通过 cin 直接读入 UTF-8 文本,均以失败告终,无论我用的是 Windows Terminal,还是经典的 conhost,还是 ConEmu、MobaXTerm:

  • system("chcp 65001");
  • SetConsoleCP
  • 修改系统全局 Code Page 到 65001

结果不是中文全部变成“0”就是变成无效的字符。

这个 SO 回答给出了部分解决这个问题的方法——使用 Windows 的 ReadConsoleW API 直接读取 UTF-16 字符串并用 WideCharToMultiByte 转换为 UTF-8。(原 po 的函数调用没有带“W”,估计是假定了编译开了 UNICODE,我这里就显式指定了)。因此我们可以写出这样的代码:

unsigned read_console_utf8(char* out, int max_size) 
{
    auto wstr = new wchar_t[max_size];
    
    unsigned read_len;
    ReadConsole(GetStdHandle(STD_INPUT_HANDLE), wstr, max_size, &read_len, NULL);
    
    int size = WideCharToMultiByte(CP_UTF8, 0, wstr, read_len, mb_str, max_size - 1, NULL, NULL);
    mb_str[size] = 0;
    
    delete[] wstr;
    
    return size;
}

但是,这样的代码并不足够:

  1. 我们想用 C++ 风格的 std::cin 来格式化输入
  2. 这个函数对换行的处理并不好
  3. 没有处理 EOF

因此,我们需要大大拓展这个方案。不妨从魔改 cin 入手。这需要实现一个自定义的 std::streambuf,然后通过 std::cin::rdbuf 替换掉 cin 的输入实现。

实现最基础的读取型 streambuf 的还是相对简单的,虽然 C++ 这块的命名只能说灾难来形容,但如果不处理那么 fancy 的功能(putback 等),那我们只需要实现 underflow 函数即可。更具体的介绍可以在 cppreference.com 参阅。

underflow 中,实现者要读取新数据,并使用 setg 函数把 streambuf 指向有读取到数据的缓冲区。同时,还要检查 gptr 是否在 egptr 前,如果在的话说明上一次数据还没有 consume 完毕,不一定要读新数据。

然而,如果看我在 utf8-console 项目中的 win_utf8_streambuf 实现,会发现里面有比较复杂的对 EOF 的处理。这是由于 Windows 的控制台下的 EOF(通过 Ctrl+Z 发送)的行为造成的。实际上在使用 ReadConsoleW 读取时,Ctrl+Z 发送的 EOF 会直接作为 0x1A 的一个 ASCII 字符读进来,且不会真正终止输入流。按 Ctrl+Z 后还可以继续读。因此我们需要判定读到的数据中有没有 0x001A(因为是 UTF-16),如果有的话需要提前结束处理,设置 EOF 标志。

在实现了 streambuf 后,创建一个全局有效的 streambuf 的实例,设置好 SetConsoleCPstd::cin.rdbuf 后就可以愉快地通过 std::cin 读取控制台下的中文了。至于换行的问题,istream 会帮我们处理好。

值得一提的是,这种方法并不会影响 scanfgetsgetcharstdin 等通过传统 C 风格 IO 的控制台函数!同时 putback 等函数也是不可用的。