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
问题
- 为什么第二(2)个结果检测到EOF中线?
EOF
前的第二个space从何而来? (这个结果最让我困惑。)
- 为什么第三 (3) 个结果会立即检测到 EOF?
- 最重要的是:如何始终始终如一地获得第一个 (1) 或最后一个结果 (4)?
环境
两台机器的环境如下:
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::getline
和 std::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,这在长 运行.
中可能更好
我对 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
问题
- 为什么第二(2)个结果检测到EOF中线?
EOF
前的第二个space从何而来? (这个结果最让我困惑。) - 为什么第三 (3) 个结果会立即检测到 EOF?
- 最重要的是:如何始终始终如一地获得第一个 (1) 或最后一个结果 (4)?
环境
两台机器的环境如下:
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::getline
和 std::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,这在长 运行.