std::wcin.eof()、UTF-8 和不同系统上的语言环境

std::wcin.eof(), UTF-8 and locales on different systems

我对 C++ 流及其对 Unicode 的处理知之甚少,试图理解为什么其他人编写的代码会以这种方式运行。如果有人能向我解释发生了什么,我将不胜感激。


MCVE:

#include <string>
#include <iostream>

int main() {
  std::basic_string<wchar_t> line;
  std::locale::global(std::locale("")); // This
  std::wcout.imbue(std::locale(""));    // This
  std::wcin.imbue(std::locale(""));     // This
  for (;;) {
    std::getline(std::wcin, line);
    if (std::wcin.eof()) {
      std::wcout << L"EOF" << std::endl;
      break;
    }
    std::wcout << line << std::endl;
  }
}

样本输入test.txt

( ) ライン
second line

编辑:test.txt 的 Hexdump:

$ xxd test.txt
00000000: 2820 2920 e383 a9e3 82a4 e383 b30a 7365  ( ) ..........se
00000010: 636f 6e64 206c 696e 650a                 cond line.

结果

在 CentOS 服务器上,这是结果 (1):

$ ./a.out < test.txt
( ) ライン
second line
EOF

在我的 Mac 虽然 (2):

$ ./a.out < test.txt
( )  EOF

如果我注释掉三个标记的语言环境行,Redhat 输出 (3):

$ ./a.out < test.txt
EOF

而 Mac 输出 (4):

$ ./a.out < test.txt
( ) ライン
second line
EOF

问题


环境

两台机器的环境如下:

CentOS Linux 发行版 7.5.1804(核心):

$ c++ --version
c++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-28)
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ locale
LANG=en_US.UTF-8
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=en_US.UTF-8

macOS Big Sur(版本 11.6):

$ c++ --version
Apple clang version 12.0.5 (clang-1205.0.22.11)
Target: x86_64-apple-darwin20.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

$ locale
LANG="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_CTYPE="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_ALL="en_US.UTF-8"

奖金

一个额外的谜题。如果我将输入更改为此(即在括号内再添加两个 space):

(   ) ライン
second line

原始(未注释)代码在 Mac:

上输出
$ ./a.out < test.txt
(   )   ララライ翕翕ン
second line
EOF

这些不是混乱终端的产物;所有这些额外的字符实际上都在那里:

$ ./a.out < test.txt | xxd
00000000: 2820 2020 2920 2020 e383 a9e3 83a9 e383  (   )   ........
00000010: a9e3 82a4 e7bf b7e7 bfb7 e383 b30a 7365  ..............se
00000020: 636f 6e64 206c 696e 650a 454f 460a       cond line.EOF.

比如……什么?

EDIT 为回应 Giacomo Catenazzi 的评论,我将 EOF 打印从 char 更改为 wide,这确实解决了一个关于输入的怪异问题。不过,我的核心问题是阅读 wcin,事实证明这是无关的。


编辑 std::getlinestd::wcin.get

之间的区别

这里是getline得到的数据。在这种情况下,我没有得到EOF,但数据仍然很奇怪:

std::wcout.imbue(std::locale("C")); // prevent commas
for (;;) {
  std::getline(std::wcin, line);
  if (std::wcin.eof()) {
    std::wcout << L"EOF" << std::endl;
    break;
  }
  int i, l = line.length();
  for (i = 0; i < l; i++) {
    wchar_t ch = line.at(i);
    std::wcout << std::hex << (int) ch << L" ";
  }
  std::wcout << std::endl;
}

输出:

28 20 29 20 20 0 30e9 30e9 30e9 30a4 30a4 30a4 30f3 
73 65 63 6f 6e 64 20 6c 69 6e 65 
EOF

0 是从哪里来的?重复的字符是怎么回事? 0 之后的字符转换为 ララライイイン。 (注意,这里我没有尝试将接收到的字符输出到wcout,只输出数值,以消除任何可能的输出编码影响。)

get得到的数据不一样,但同样奇怪:

// ...
std::wcout.imbue(std::locale("C")); // prevent commas
for (;;) {
  wchar_t ch = std::wcin.get();
  if (std::wcin.eof()) {
    std::wcout << L"EOF" << std::endl;
    break;
  }
  std::wcout << std::hex << (int) ch << L" ";
  if (std::char_traits<wchar_t>::eq(ch, std::wcin.widen('\n'))) {
    std::wcout << std::endl;
  }
}

输出:

28 20 29 20 7ffe 7ffe 30e9 7ffe 7ffe 30a4 7ffe 7ffe 30f3 a 
73 65 63 6f 6e 64 20 6c 69 6e 65 a 
EOF

这转换为 翾翾ラ翾翾イ翾翾ン。那些 7ffe 个字符来自哪里?

这是一个libc++ bug

请注意错误报告说它只影响 std::wcin 而不是文件流,但在我的实验中情况并非如此。所有 wchar_t 流似乎都受到影响。

另一个主要的开源实现 libstdc++ 没有这个错误。可以通过针对 libstdc++ 构建整个应用程序(包括所有动态库,如果有的话)来回避 libc++ 错误。

如果这不是一个选项,那么解决该错误的一种方法是使用窄 char 流,然后在需要时重新编码字符(可能到达编码为 UTF-8)以wchar_t(大概是 UCS-4)分开。另一种方法是完全摆脱 wchar_t 并在整个程序中使用 UTF-8,这在长 运行.

中可能更好