正如你在 Exploration 18 中看到的,C++ 提供了一个复杂的系统来支持你的代码的国际化和本地化。即使您不打算将程序翻译成多种语言,您也必须了解 C++ 使用的语言环境机制。事实上,您一直在使用它,因为 C++ 总是通过 locale 系统发送格式化的 I/O。这种探索将帮助您更好地理解区域设置,并在您的程序中更有效地使用它们。
巴别塔的故事引起了程序员的共鸣。想象一个说单一语言和使用单一字母表的世界。如果我们不必处理字符集问题、语言规则或地区,编程将会简单得多。
现实世界有许多语言、无数的字母和音节表以及众多的字符集,所有这些都使生活更加丰富和有趣,也使程序员的工作更加困难。不管怎样,我们程序员必须应付。这并不容易,这次探索不能给你所有的答案,但这是一个开始。
不同的文化、语言和字符集会产生不同的信息呈现和解释方法、不同的字符代码解释(正如您在 Exploration 18 中了解到的),以及不同的信息组织(尤其是分类)方式。即使是数字数据,您可能会发现,根据当地的环境、文化和语言,您必须以几种方式书写相同的数字。表格 58-1 展示了一些根据不同文化、习俗和地区书写数字的方法。
表 58-1。
写数字的各种方法
|数字
|
文化
| | --- | --- | | 123456.7890 | 默认 C++ | | 123,456.7890 | 美国 | | 123 456.7890 | 国际科学 | | 卢比 1,23,456.7890 | 印度货币 * | | 123.456,7890 | 德国 |
*** 是的,逗号是正确的。
其他文化差异包括
-
12 小时对。24 小时制
-
时区
-
夏令时实践
-
重音字符相对于非重音字符是如何排序的(
'a'
在'á'
之前还是之后)?) -
日期格式:月/日/年、日/月/年或年-月-日
-
货币格式(123,456 或 99)
不知何故,糟糕的应用程序程序员必须弄清楚什么是文化相关的,收集应用程序可能运行的所有可能的文化的信息,并在应用程序中适当地使用这些信息。幸运的是,艰苦的工作已经为您完成了,并且是 C++ 标准库的一部分。
C++ 使用一个名为 locales 的系统来管理这种风格差异。探索 18 引入了语言环境作为组织字符集及其属性的手段。地区还组织数字、货币、日期和时间的格式(加上一些我不会深入讨论的东西)。
C++ 定义了一个基本的语言环境,称为经典语言环境,它提供了最少的格式。每个 C++ 实现都可以自由地提供额外的语言环境。每个语言环境通常都有一个名称,但是 C++ 标准并没有强制要求任何特定的命名约定,这使得编写可移植代码变得很困难。您只能依赖两个标准名称:
-
经典的地点被命名为
"C"
。经典语言环境为所有实现指定了相同的基本格式信息。当程序启动时,经典区域设置是初始区域设置。 -
空字符串(
""
)表示默认的或本地语言环境。默认区域设置从主机操作系统获取格式和其他信息,获取方式取决于操作系统所能提供的内容。对于传统的桌面操作系统,您可以假设默认区域设置指定了用户首选的格式规则和字符集信息。对于其他环境,如嵌入式系统,默认的语言环境可能与经典的语言环境相同。
许多 C++ 实现使用 ISO 和 POSIX 标准来命名地区:语言的 ISO 639 代码(例如,fr
代表法语,en
代表英语,ko
代表韩语),可选地后跟下划线和地区的 ISO 3166 代码(例如,CH
代表瑞士,GB
代表英国,HK
代表香港)。该名称可选地后跟一个点和字符集的名称(例如,utf8
用于 Unicode UTF-8,Big5
用于中文 Big 5 编码)。因此,我使用en_US.utf8
作为我的默认语言环境。一个台湾本地人可能会用zh_TW.Big5
;瑞士法语区的开发者可能会使用fr_CH.latin1
。阅读您的库文档,了解它是如何指定区域名称的。您的默认区域设置是什么?_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _它的主要特点是什么?
每个 C++ 应用程序都有一个全局locale
对象。除非您显式地更改流的区域设置,否则它会从全局区域设置开始。(如果您稍后更改了全局语言环境,这不会影响已经存在的流,例如标准 I/O 流。)最初,全局语言环境是经典语言环境。经典语言环境在任何地方都是一样的(除了依赖于字符集的部分),所以程序在经典语言环境下具有最大的可移植性。另一方面,它有最低限度的地方风味。下一节将探讨如何改变流的语言环境。
回想一下 Exploration 18 中,您在流中注入了一个语言环境,以便根据语言环境的规则格式化 I/O。因此,为了确保以经典语言环境读取输入,并以用户的本地语言环境打印结果,您需要:
std::cin.imbue(std::locale::classic()); // standard input uses the classic locale
std::cout.imbue(std::locale{""}); // imbue with the user's default locale
标准的 I/O 流最初使用经典的语言环境。您可以在任何时候用新的语言环境来填充流,但是在执行任何 I/O 之前这样做最有意义。
通常,在读取或写入文件时,您会使用经典区域设置。您通常希望文件的内容是可移植的,并且不依赖于用户的操作系统偏好。对于控制台或 GUI 窗口的短暂输出,您可能希望使用默认的区域设置,这样用户可以最舒适地阅读和理解它。另一方面,如果另一个程序可能试图读取您程序的输出(就像 UNIX 管道和过滤器一样),您应该坚持使用传统的语言环境,以确保可移植性和通用格式。如果您准备在 GUI 中显示输出,请务必使用默认的语言环境。
流解释数字输入和格式化数字输出的方式是通过请求被灌输的语言环境。一个对象是一个片段的集合,每个片段管理国际化的一个小方面。例如,一个名为numpunct
的组件提供了数字格式的标点符号,比如小数点字符(在美国是'.'
,但在法国是','
)。另一个片段,num_get
,使用从numpunct
获得的信息,从流中读取并解析文本以形成一个数字。num_get
和numpunct
等棋子称为刻面。
对于普通的数值 I/O,您永远不必处理刻面。I/O 流自动为您管理这些细节:operator<<
函数使用num_put
方面来格式化输出的数字,而operator>>
使用num_get
将文本解释为数字输入。对于货币、日期和时间,I/O 操纵器使用刻面来格式化值。但是有时候你需要自己使用刻面。你在探索 18 中学到的isalpha
、toupper
和其他与角色相关的功能都使用ctype
刻面。任何必须进行大量字符测试和转换的程序都可以通过直接管理其方面而受益。
像字符串和 I/O 流一样,刻面是类模板,在字符类型上参数化。到目前为止,你唯一用过的字符类型是char
;你将在探索中了解其他角色类型。不管字符类型如何,原则都是一样的(这就是刻面使用模板的原因)。
要从一个地区获得一个方面,调用use_facet
函数模板。模板参数是你寻找的 facet,函数参数是locale
对象。返回的方面是const
,不可复制,所以使用结果的最佳方式是初始化一个const
引用,如下所示:
auto const& mget{ std::use_facet<std::money_get<char>>(std::locale{""}) };
从内向外读取,名为mget
的对象被初始化为调用use_facet
函数的结果,该函数请求对money_get<char>
方面的引用。默认语言环境作为唯一的参数传递给use_facet
函数。mget
的类型是对const money_get<char>
刻面的引用。一开始读起来有点令人生畏,但你最终会习惯的。
直接使用刻面可能会很复杂。幸运的是,标准库提供了一些 I/O 操纵器(在<iomanip>
中声明)来简化时间和货币方面的使用。清单 58-1 展示了一个简单的程序,它将标准 I/O 流融入本地语言环境,然后读取和写入货币值。
import <iomanip>;
import <iostream>;
import <locale>;
import <string>;
int main()
{
std::locale native{""};
std::cin.imbue(native);
std::cout.imbue(native);
std::cin >> std::noshowbase; // currency symbol is optional for input
std::cout << std::showbase; // always write the currency symbol for output
std::string digits;
while (std::cin >> std::get_money(digits))
{
std::cout << std::put_money(digits) << '\n';
}
if (not std::cin.eof())
std::cout << "Invalid input.\n";
}
Listing 58-1.Reading and Writing Currency Using the Money I/O Manipulators
区域设置操纵器像其他操纵器一样工作,但是它们调用相关的方面。操纵器使用流来处理错误标志、迭代器、填充字符等等。get_time
和put_time
操纵器读取和写入日期和时间;详情请查阅库参考资料。
这一部分继续你在《T2》18 中开始的字符集和区域设置的检查。除了测试字母数字字符或小写字符,您还可以测试几种不同的类别。表 58-2 列出了所有的分类函数以及它们在经典语言环境中的行为。都是以一个字符作为第一个参数,以一个locale
作为第二个参数;它们都返回一个bool
结果。
表 58-2。
字符分类功能
|功能
|
描述
|
经典区域设置
|
| --- | --- | --- |
| isalnum
| 含字母和数字的 | 'a'
–'z'
、'A'
–'Z'
、'0'
–'9'
|
| isalpha
| 字母的 | 'a'
–'z'
,'A'
–'Z'
|
| iscntrl
| 控制 | 任何不可打印的字符 |
| isdigit
| 手指 | '0'
–'9'
(所有地区) |
| isgraph
| 图解的 | 除' '
以外的可打印字符 |
| islower
| 小写字母 | 'a'
–'z'
|
| isprint
| 可印刷的 | 字符集 中的任何可打印字符 |
| ispunct
| 标点 | 除字母数字或空白以外的可打印字符 |
| isspace
| 空格 | ' '
、'\f'
、'\n'
、'\r'
、'\t'
、'\v'
|
| isupper
| 大写字母 | 'A'
–'Z'
|
| isxdigit
| 十六进制数字 | 'a'
–'f'
、'A'
–'F'
、'0'
–'9'
(所有地区) |
* 行为取决于字符集,即使在经典的语言环境中也是如此。
经典语言环境对一些类别有固定的定义(比如isupper
)。然而,其他地区可以扩展这些定义以包含其他字符,这些字符可能(也可能)依赖于字符集。只有isdigit
和isxdigit
对所有地区和所有字符集都有固定的定义。
然而,即使在经典的语言环境中,一些函数的精确实现,比如isprint
,也依赖于字符集。例如,在流行的 ISO 8859-1 (Latin-1)字符集中,'\x80'
是一个控制字符,但是在同样流行的 Windows-1252 字符集中,它是可打印的。在 UTF-8 中,'\x80'
是无效的,所以所有的分类函数都将返回false
。
语言环境和字符集之间的交互是 C++ 表现不佳的地方之一。区域设置可以随时更改,这可能会设置一个新的字符集,从而赋予某些字符值新的含义。但是,编译器对字符集的看法是固定的。例如,编译器将'A'
视为大写罗马字母 A ,并根据其运行时字符集的概念编译数字代码。该数值将永远固定不变。如果特征函数使用相同的字符集,一切都很好。isalpha
和isupper
函数返回真;isdigit
返回 false 这个世界一切都好。如果用户更改了区域设置,从而更改了字符集,那么这些函数可能不再适用于该字符变量。
让我们考虑一个具体的例子,如清单 58-2 所示。此程序对区域名称进行编码,这可能不适合您的环境。阅读评论,看看您的环境是否可以支持相同类型的语言环境,尽管名称不同。你将需要清单 40-4 中的ioflags
类。将类复制到它自己的模块ioflags
中,或者从书的网站下载文件。在阅读清单 58-2 ,之后,您期望的结果是什么?
import <format>;
import <iostream>;
import <locale>;
import <ostream>;
import ioflags; // from Listing 40-4
/// Print a character's categorization in a locale.
void print(int c, std::string const& name, std::locale loc)
{
// Don't concern yourself with the & operator. I'll cover that later
// in the book, in Exploration 63\. Its purpose is just to ensure
// the character's escape code is printed correctly.
std::cout << std::format("\\x{:02x} is {} in {}\n", c & 0xff, name, loc.name());
}
/// Test a character's categorization in the locale, @p loc.
void test(char c, std::locale loc)
{
ioflags save{std::cout};
if (std::isalnum(c, loc))
print(c, "alphanumeric", loc);
else if (std::iscntrl(c, loc))
print(c, "control", loc);
else if (std::ispunct(c, loc))
print(c, "punctuation", loc);
else
print(c, "none of the above", loc);
}
int main()
{
// Test the same code point in different locales and character sets.
char c{'\xd7'};
// ISO 8859-1 is also called Latin-1 and is widely used in Western Europe
// and the Americas. It is often the default character set in these regions.
// The country and language are unimportant for this test.
// Choose any that support the ISO 8859-1 character set.
test(c, std::locale{"en_US.iso88591"});
// ISO 8859-5 is Cyrillic. It is often the default character set in Russia
// and some Eastern European countries. Choose any language and region that
// support the ISO 8859-5 character set.
test(c, std::locale{"ru_RU.iso88595"});
// ISO 8859-7 is Greek. Choose any language and region that
// support the ISO 8859-7 character set.
test(c, std::locale{"el_GR.iso88597"});
// ISO 8859-8 contains some Hebrew
. The character set is no longer widely used.
// Choose any language and region that support the ISO 8859-8 character set.
test(c, std::locale{"he_IL.iso88598"});
}
Listing 58-2.Exploring Character Sets and Locales
你得到的实际回应是什么?
如果您在识别区域名称或运行该程序时遇到其他问题,以下是我在系统上运行该程序时得到的结果:
\xd7 is punctuation in en_US.iso88591
\xd7 is alphanumeric in ru_RU.iso88595
\xd7 is alphanumeric in el_GR.iso88597
\xd7 is none of the above in he_IL.iso88598
正如您所看到的,相同的字符有不同的类别,这取决于地区的字符集。现在假设用户输入了一个字符串,你的程序存储了这个字符串。如果您的程序更改了全局区域设置或用于处理该字符串的区域设置,您最终可能会误解该字符串。
在清单 58-2 中,分类函数在每次被调用时都会重新加载它们的方面,但是你可以重写程序,让它只加载一次它的方面。字符型刻面叫做ctype
。它有一个名为is
的函数,将类别掩码和字符作为参数,如果字符在掩码中有类型,则返回一个bool
: true。屏蔽值在std::ctype_base
中指定。
Note
请注意标准库自始至终使用的约定。当类模板需要助手类型和常量时,它们在非模板基类中声明。类模板派生自基类,因此可以轻松访问类型和常量。调用方通过用基类名称限定来获得对类型和常量的访问权。通过避免在基类中使用模板,标准库避免了仅仅为了使用与模板参数无关的类型或常量而进行的不必要的实例化。
掩码名称与分类函数相同,但没有前导的is
。清单 58-3 展示了如何重写简单的字符集演示来使用一个缓存的ctype
方面。
import <format>;
import <iostream>;
import <locale>;
import ioflags; // from Listing 40-4
void print(int c, std::string const& name, std::locale loc)
{
// Don't concern yourself with the & operator. I'll cover that later
// in the book. Its purpose is just to ensure the character's escape
// code is printed correctly.
std::cout << std::format("\\x{:02x} is {} in {}\n", c & 0xff, name, loc.name());
}
/// Test a character's categorization in the locale, @p loc.
void test(char c, std::locale loc)
{
ioflags save{std::cout};
std::ctype<char> const& ctype{std::use_facet<std::ctype<char>>(loc)};
if (ctype.is(std::ctype_base::alnum, c))
print(c, "alphanumeric", loc);
else if (ctype.is(std::ctype_base::cntrl, c))
print(c, "control", loc);
else if (ctype.is(std::ctype_base::punct, c))
print(c, "punctuation", loc);
else
print(c, "none of the above", loc);
}
int main()
{
// Test the same code point
in different locales and character sets.
char c{'\xd7'};
// ISO 8859-1 is also called Latin-1 and is widely used in Western Europe
// and the Americas. It is often the default character set in these regions.
// The country and language are unimportant for this test.
// Choose any that support the ISO 8859-1 character set.
test(c, std::locale{"en_US.iso88591"});
// ISO 8859-5 is Cyrillic. It is often the default character set in Russia
// and some Eastern European countries. Choose any language and region that
// support the ISO 8859-5 character set.
test(c, std::locale{"ru_RU.iso88595"});
// ISO 8859-7 is Greek. Choose any language and region that
// support the ISO 8859-7 character set.
test(c, std::locale{"el_GR.iso88597"});
// ISO 8859-8 contains some Hebrew. It is no longer widely used.
// Choose any language and region that support the ISO 8859-8 character set.
test(c, std::locale{"he_IL.iso88598"});
}
Listing 58-3.Caching the ctype Facet
ctype
方面还使用toupper
和tolower
成员函数执行大小写转换,这两个函数接受一个字符参数并返回一个字符结果。回忆探险第 22 期的计字题。重写您的解决方案(参见清单 23-2 和23-3)并更改 sanitize()函数以使用缓存的 facet 。我建议用一个sanitizer
类替换这个函数,这样这个类就可以在数据成员中存储这个方面。将你的程序与清单 58-4 进行比较。
import <format>;
import <iostream>;
import <locale>;
import <map>;
import <ranges>;
import <string>;
import <string_view>;
using count_map = std::map<std::string, int>; ///< Map words to counts
using count_pair = count_map::value_type; ///< pair of a word and a count
using str_size = std::string::size_type; ///< String size type
void initialize_streams()
{
std::cin.imbue(std::locale{});
std::cout.imbue(std::locale{});
}
class sanitizer
{
public:
sanitizer(std::locale const& locale)
: ctype_{ std::use_facet<std::ctype<char>>(locale) }
{}
bool keep(char ch) const { return ctype_.is(ctype_.alnum, ch); }
char tolower(char ch) const { return ctype_.tolower(ch); }
std::string operator()(std::string_view str)
const
{
auto data{ str
| std::ranges::views::filter([this](char ch) { return keep(ch); })
| std::ranges::views::transform([this](char ch) { return tolower(ch); }) };
return std::string{ std::ranges::begin(data), std::ranges::end(data) };
}
private:
std::ctype<char> const& ctype_;
};
str_size get_longest_key(count_map const& map)
{
str_size result{0};
for (auto const& pair : map)
if (pair.first.size() > result)
result = pair.first.size();
return result;
}
void print_pair(count_pair const& pair, str_size longest)
{
int constexpr count_size{10}; // Number of places for printing the count
std::cout << std::format("{0:{1}} {2:{3}}\n", pair.first, longest, pair.second, count_size);
}
void print_counts(count_map const& counts)
{
auto longest{get_longest_key(counts)};
// For each word/count pair...
for (count_pair pair: counts)
print_pair(pair, longest);
}
int main()
{
// Set the global locale to the native locale.
std::locale::global(std::locale{""});
initialize_streams();
count_map counts{};
sanitizer sanitize{std::locale{""}};
// Read words from the standard input and count the number of times
// each word occurs.
std::string word{};
while (std::cin >> word)
{
std::string copy{sanitize(word)};
// The "word" might be all punctuation, so the copy would be empty.
// Don't count empty strings.
if (not copy.empty())
++counts[copy];
}
print_counts(counts);
}
Listing 58-4.Counting Words Again, This Time with Cached Facets
请注意,程序的大部分内容都没有改变。在我的系统上,缓存ctype
facet 的简单行为减少了这个程序大约 15%的运行时间。
可以将关系运算符(如<
)与字符和字符串一起使用,但它们实际上并不比较字符或码位;他们比较存储单元。大多数用户并不关心一个名字列表是否按照存储单元按数字升序排序。他们想要一个按照他们自己的排序规则按字母升序排列的名字列表。
比如哪个先来:昂斯特伦还是角度?答案取决于你住在哪里,说什么语言。在斯堪的纳维亚,角度在前,昂斯特伦在斑马后。collate
方面根据地区的规则比较字符串。它的compare
函数使用起来有些笨拙,所以locale
类模板提供了一个简单的接口来确定一个string
是否比另一个少:使用locale
的函数调用操作符。换句话说,您可以使用一个locale
对象本身作为标准算法的比较函子,比如sort
。清单 58-5 显示了一个程序,该程序演示了排序规则如何依赖于区域设置。为了让程序在您的环境中运行,您可能需要更改区域名称。
import <algorithm>;
import <iostream>;
import <iterator>;
import <locale>;
import <string>;
import <vector>;
void sort_words(std::vector<std::string> words, std::locale loc)
{
std::ranges::sort(words, loc);
std::cout << loc.name() << ":\n";
std::ranges::copy(words,
std::ostream_iterator<std::string>(std::cout, "\n"));
}
int main()
{
std::vector<std::string> words{
"circus",
"\u00e5ngstrom", // ångstrom
"\u00e7irc\u00ea", // çircê
"angle",
"essen",
"ether",
"\u00e6ther", // æther
"aether",
"e\u00dfen" // eßen
};
sort_words(words, std::locale::classic());
sort_words(words, std::locale{"en_GB.utf8"}); // Great Britain
sort_words(words, std::locale{"no_NO.utf8"}); // Norway
}
Listing 58-5.Demonstrating How Collation Order Depends on Locale
\uNNNN
字符是一种表达 Unicode 字符的可移植方式。NNNN
必须是四个十六进制数字,指定一个 Unicode 码位。在接下来的探索中你会学到更多。
粗体行显示了如何使用locale
对象作为比较函子对单词进行排序。表 58-3 列出了我在每个地区得到的结果。根据您的本地字符集,您可能会得到不同的结果。
表 58-3。
各种语言环境的归类顺序
|经典的
|
大不列颠
|
挪威
|
| --- | --- | --- |
| aether
| aether
| aether
|
| angle
| æther
| angle
|
| circus
| angle
| çircê
|
| essen
| ångstrom
| circus
|
| ether
| çircê
| essen
|
| eßen
| circus
| eßen
|
| ångstrom
| essen
| ether
|
| æther
| eßen
| æther
|
| çircê
| ether
| ångstrom
|
下一篇文章将深入探讨 Unicode、国际字符集以及相关的挑战。