C++编程语言:标准库:第39章——本地化(语言环境)( Locales)(Bjarne Stroustrup)

第 39章 本地化(语言环境)( Locales)

(译注:以下交替使用"本地化"和"语言环境"这两种译名。)

目录

[39.1 处理文化差异(Handling Cultural Differences)](#39.1 处理文化差异(Handling Cultural Differences))

[39.2 类locale (Class locale)](#39.2 类locale (Class locale))

[39.2.1 命名locale (Named locales)](#39.2.1 命名locale (Named locales))

[39.2.1.1 构造新的locale (Constructing New locales)](#39.2.1.1 构造新的locale (Constructing New locales))

[39.2.2 比较字符串 (Comparing strings)](#39.2.2 比较字符串 (Comparing strings))

[39.3 类facet (Class facet)](#39.3 类facet (Class facet))

[39.3.1 在locale中访问facets (Accessing facets in a locale)](#39.3.1 在locale中访问facets (Accessing facets in a locale))

[39.3.2 一个简单的用户定义facet (A Simple User-Defined facet)](#39.3.2 一个简单的用户定义facet (A Simple User-Defined facet))

[39.3.3 使用locale和facet (Uses of locales and facets)](#39.3.3 使用locale和facet (Uses of locales and facets))

[39.4 标准 facet (Standard facets)](#39.4 标准 facet (Standard facets))

[39.4.1 string 比较 (string Comparison)](#39.4.1 string 比较 (string Comparison))

[39.4.1.1 命名collate(Named collate)](#39.4.1.1 命名collate(Named collate))

[39.4.2 数值格式化(Numeric Formatting)](#39.4.2 数值格式化(Numeric Formatting))

[39.4.2.1 数值标点(Numeric Punctuation)](#39.4.2.1 数值标点(Numeric Punctuation))

[39.4.2.2 数值输出(Numeric Output)](#39.4.2.2 数值输出(Numeric Output))

[39.4.2.3 数值输入(Numeric Input)](#39.4.2.3 数值输入(Numeric Input))

[39.4.3 货币格式化(Money Formatting)](#39.4.3 货币格式化(Money Formatting))

[39.4.3.1 货币标点(Money Punctuation)](#39.4.3.1 货币标点(Money Punctuation))

[39.4.3.2 货币输出(Money Output)](#39.4.3.2 货币输出(Money Output))

[39.4.3.3 货币输入(Money Input)](#39.4.3.3 货币输入(Money Input))

[39.4.4 日期和时间格式化(Date and Time Formatting)](#39.4.4 日期和时间格式化(Date and Time Formatting))

[39.4.4.1 time_put](#39.4.4.1 time_put)

[39.4.4.2 time_get](#39.4.4.2 time_get)

[39.4.5 字符分类(Character Classification)](#39.4.5 字符分类(Character Classification))

[39.4.6 字符编码转换(Character Code Conversion)](#39.4.6 字符编码转换(Character Code Conversion))

[39.4.7 消息(Messages)](#39.4.7 消息(Messages))

[39.4.7.1 使用来自其它 facet 的消息(Using Messages from Other facets)](#39.4.7.1 使用来自其它 facet 的消息(Using Messages from Other facets))

[39.5 便捷接口(Convenience Interfaces)](#39.5 便捷接口(Convenience Interfaces))

[39.5.1 字符分类(Character Classification)](#39.5.1 字符分类(Character Classification))

[39.5.2 字符转换(Character Conversions)](#39.5.2 字符转换(Character Conversions))

[39.5.3 字符串转换(String Conversions)](#39.5.3 字符串转换(String Conversions))

[39.5.4 缓冲区转换(Buffer Conversions)](#39.5.4 缓冲区转换(Buffer Conversions))

[39.6 建议(Advice)](#39.6 建议(Advice))


39.1 处理文化差异( Handling Cultural Differences**)**

locale 是一个表示一组文化偏好的对象,例如字符串的比较方式、数字作为人类可读输出的显示方式以及字符在外部存储中的表示方式。本地化(语言环境)的概念是可扩展的,因此程序员可以向语言环境添加新的facet ,这些facet 表示标准库不直接支持的特定于语言环境的实体,例如邮政编码和电话号码。标准库中语言环境的主要用途是控制写入 ostream 的信息的外观以及 istream 读取的数据的格式。

本章介绍如何使用locale ,如何使用facet 构建locale 以及locale如何影响 I/O 流。

本地化的概念并非主要存在于 C++ 中。大多数操作系统和应用程序环境都具有本地化的概念。在原则上,该概念在系统上的所有程序之间共享,无论它们使用哪种编程语言编写。因此,C++ 标准库中的本地化概念可以视为一种标准且可移植的方式,供 C++ 程序访问在不同系统上具有截然不同表示方式的信息。 此外,C++ locale还是一个系统信息的接口,这些信息在不同系统上的表示方式可能不兼容。

假设你要编写一个需要在多个国家/地区使用的程序。以允许这种情况发生的风格编写程序通常被称为国际化 (internationalization)(强调程序可在多个国家/地区使用)或本地化(localization)(强调程序能够适应当地情况)。程序操作的许多实体通常会在这些国家/地区以不同的方式显示。我们可以通过编写 I/O 例程来解决这个问题,以将这种情况考虑在内。例如:

void print_date(const Date& d) // print in the appropriate for mat

{

switch(where_am_I) { // user-defined style indicator

case DK: // e.g., 7. marts 1999

cout << d.day() << ". " << dk_month[d.month()] << " " << d.year();

break;

case ISO: // e.g., 1999-3-7

cout << d.year() << " − " << d.month() << " / " << d.day();

break;

case US: // e.g., 3/7/1999

cout << d.month() << "/" << d.day() << "/" << d.year();

break;

// ...

}

}

这种代码风格确实能满足需求。然而,这种代码很丑陋,而且难以维护。特别是,我们必须始终如一地使用这种风格,以确保所有输出都根据本地习惯进行了适当的调整。如果我们想添加一种新的日期书写方式,就必须修改应用程序代码。更糟糕的是,日期书写方式只是众多文化差异的例子之一。

因此,标准库提供了一种可扩展的方式来处理文化习俗。iostream库依赖此框架来处理内置类型和用户定义类型(§38.1)。例如,考虑一个简单的可能表示一系列测量值或一组事务的循环复制(Date,double) 对:

void cpy(istream& is, ostream& os)// copy (Date,double) stream

{

Date d;

double volume;

while (is >> d >> volume)

os << d << ' '<< volume << '\n';

}

当然,真正的程序会对记录做一些事情,理想情况下也会对错误处理更加小心。如何让这个程序读取一个符合法国格式的文件(法国格式中,逗号是用来表示浮点数小数点的字符;例如, 12,5 表示十二点五),并按照美国格式写入? 我们可以定义locale 和 I/O 操作,以便使用**cpy()**在两种格式之间进行转换:

void f(istream& fin, ostream& fout, istream& fin2, ostream& fout2)

{

fin.imbue(locale{"en_US.UTF −8"}); // American English

fout.imbue(locale{"fr_FR.UTF −8"}); // French

cpy(fin,fout); //read American English, write French

// ...

fin2.imbue(locale{"fr_FR.UTF −8"}); // French

fout2.imbue(locale{"en_US.UTF −8"}); // American English

cpy(fin2,fout2); //read French, write American English

// ...

}

已知这些流为:

Apr 12, 1999 1000.3

Apr 13, 1999 345.45

Apr 14, 1999 9688.321

...

3 juillet 1950 10,3

3 juillet 1951 134,45

3 juillet 1952 67,9

...

则这个程序会产生:

12 avril 1999 1000,3

13 avril 1999 345,45

14 avril 1999 9688,321

...

July 3, 1950 10.3

July 3, 1951 134.45

July 3, 1952 67.9

...

本章的其余部分将致力于描述实现这一目标的机制并解释如何使用它们。然而,大多数程序员几乎没有理由处理locale 的细节,并且永远不会明确地操作locale 。他们最多只会检索一个标准语言环境并将其注入(imbue)到流中(§38.4.5.1)。

本地化(国际化)的概念很简单。然而,实际的限制使得 locale 的设计和实现相当复杂:

1\] **locale**封装了文化习俗,例如日期的显示方式。这些习俗在许多细微且不系统的方式上存在差异。这些习俗与编程语言无关,因此编程语言无法对其进行标准化。 \[2\] **locale**的概念必须是可扩展的,因为不可能列举出所有对每一个 C++ 用户都重要的文化习俗。 \[3\] **locale**用于人们要求运行时效率的操作(例如,I/O 和排序)。 \[4\] 对于大多数程序员来说,**locale**必须是不可见的,他们希望从"做正确的事情"的设施中获益,而不必确切知道"正确的事情"是什么或如何实现。 \[5\] 处理超出标准范围的文化敏感信息的设施设计人员必须能够使用**locale**。 这些用于组合这些**locale**并使其易于使用的机制构成了它们自己的一种小型编程语言。 **locale** 由控制各个方面的**facet** 组成,例如,浮点值输出中使用的标点符号(**decimal_point()** ;§39.4.2)以及读取货币值的格式(**moneypunct** ;§39.4.3)。**facet** 是从类 **locale::facet** (§39.3)派生的类的对象。我们可以将**locale** 视为**facet**(§39.2、§39.3.1)的容器。 ## **39.2** **类** **locale** **(** Class **locale** **)** **locale** 类及其相关设施呈现于**\**。 |---------------------------------|------------------------------------------------------------------------------------------------------------------------------| | **locale** 成员**(§iso.22.3.1)** || | **locale loc {};** | **loc**是当前全局语言环境的副本;noexcept | | **locale loc {loc2};** | 复制构造函数:**loc** 存储 **loc2** 的副本;**loc.name()==loc2.name()**;noexcept | | **locale loc {s};** | 将 **loc** 初始化为具有名称 **s** 的**locale** ;**s** 可以是**string** 或 C 风格字符串;**loc.name()==s** **;**explicit | | **locale loc {loc2,s,cat};** | **loc** 是 **loc2** 的副本,除了类别为 **cat** 的方面,它是从 **locale{s}** 复制而来的;**s** 可以是**string** 或 C 风格字符串;如果**loc2** 有名称,则 **loc** 也有名称 | | **locale loc {loc2,pf};** | **loc** 是 **loc2** 的一个副本,除了对于**facet** **∗** **pf** 前提是 **pf!=nullptr** ;**loc** 没有名称 | | **locale loc {loc2,loc3,cat};** | **loc** 是 **loc2** 的副本,除了 **cat** 类别的facet,它是从 **loc3** 复制而来的;如果 **loc2** 和**loc3** 都有名称,那么**loc**也有一个名称 | | **loc.** **˜locale()** | 析构函数;非虚函数;noexcept | | **loc2=loc** | 赋值:**loc2** 是 **loc** 的副本;noexcept | | **loc3=loc.combine\(loc2)** | **loc3** 是 **loc** 的一个副本,除了**facet F**, 而 **facet F** 是从 **loc2** 复制而来的;**loc3**没有名称 | | **s=loc.name()** | **s** 是 **loc** 的语言环境名称或**"** **∗** **"** | | **loc==loc2** | **loc** 和 **loc2** 是同一个**˜locale**吗? | | **loc!=loc2** | **!(loc==loc2)** | | **loc()(s,s2)** | 使用 **loc** 的 **collat** **e\** 方面比较 **basic_string\s** 和 **s2** | | **loc2=global(loc)** | 将全局语言环境设置为 **loc** ;**loc2** 是前一个全局**locale** | | **loc=classic()** | **loc**是经典的"C"语言环境 | 如果已知名称的**locale** 或所引用的**facet** 不存在,则命名它的**locale** 操作将引发**runtime_error**。 **locale** 的命名有点奇怪。当你基于另一个**locale** 加上一个**facet** 创建新的**locale** 时,并且生成的**locale** 有一个名称时,该名称是由实现定义的。通常,这种由实现定义的名称包含提供大部分**facet** 的**locale** 的名称。对于没有名称的**locale** ,**name()** 返回**"** **∗** **"**。 可以将**locale** 视为 **map\** 的接口,也就是说,它允许我们使用 **locale::id** 来查找 **locale::facet** 派生类的对应对象。**locale** 的实际实现是这种思想的有效变体。布局如下所示: ![](https://i-blog.csdnimg.cn/direct/5391613ac22b4f0ca29a9defedbcfa33.png) 这里,**collat** **e\** 和**numpunct\** 是标准库 **facet** (§39.4)。所有 **facet** 均派生自**locale::facet**。 **locale** **旨在自由且低成本地复制** 。因​​此,**locale** 几乎肯定会实现为构成其主要实现部分的特定 **map\** 的句柄。在**locale** 中,各个方面必须能够快速访问。因此,特定**map\** 将被优化以提供类似数组的快速访问。**locale** 的**facet** 可以通过 **use_facet\(loc)**表示法访问;参见 §39.3.1。 标准库提供了丰富的**facet** 。为了帮助程序员按逻辑分组操作**facet** ,标准**facet** 分为不同的类别,例如**numeric** 和**collate** (§39.4): |--------------|-----------------------------------------------------------------| | **facet** **分类** **(§iso.22.3.1)** || | **collate** | 例如, **collate** ; §39.4.1 | | **ctype** | 例如,**ctype** ; §39.4.5 | | **numeric** | 例如,**num_put, num_g et, numpunct;** §39.4.2 | | **monetary** | **money_put, money_g et, moneypunct;** §39.4.3 | | **time** | 例如,**time_put, time_get;** §39.4.4 | | **messages** | **messages** ; §39.4.7 | | **all** | **collate \| ctype \| monetary \| numeric \| time \| messages** | | **none** | | 程序员无法为新创建的**locale** 指定名称字符串。名称字符串要么在程序的执行环境中定义,要么由**locale**构造函数创建为这些名称的组合。 程序员可以替换现有类别中的**facet**(§39.4,§39.4.2.1)。但是,程序员无法定义新的类别。"类别"的概念仅适用于标准库facet,并且不可扩展。因此,facet不必属于任何类别,许多用户定义的facet也并非如此。 如果**locale** **x** 没有名称字符串,则**locale::global(x)** 是否影响 C 全局语言环境尚未定义。这意味着 C++ 程序无法可靠且可移植地将 C 语言环境设置为未从执行环境中检索的语言环境。C 程序没有设置 C++ 全局语言环境的标准方法(除非调用 C++ 函数)。在 C 和 C++ 混合程序中,使 C 全局语言环境与**global()**不同容易出错。 到目前为止,**locale** 的主要用途是隐式地应用于流 I/O 中。每个 **istream** 和 **ostream** 都有其自己的**locale** 。默认情况下,流的**locale** 是其创建时的全局**locale** (§39.2.1)。流的**locale** 可以通过 **imbue()** 操作设置,我们也可以使用 **getloc()** (§38.4.5.1) 提取流**locale**的副本。 设置全局**locale** 不会影响现有的 I/O 流;这些 I/O 流仍然使用重置全局**locale**之前所包含的语言环境。 ### **39.2.1** **命名** **locale** **(** Named **locale** s**)** 一个**locale** 由另一个**locale** 和一些**facet** 构成。创建**locale**最简单的方法是复制一个现有的语言环境。例如: **locale loc1; //** copy of the current global locale **locale loc2 {""}; //** copy of ''the user's preferred locale'' **locale loc3 {"C"}; //** copy of the ''C'' locale **locale loc4 {locale::classic()}; //** copy of the ''C'' locale **locale loc5 {"POSIX"}; //** copy of the locale named "POSIX" **locale loc6 {"Danish_Denmark.1252"}; //** copy of the locale named "Danish_Denmark.1252" **locale loc7 {"en_US.UTF** **−8"}; //** copy of the locale named "en_US.UTF-8" 根据标准定义,**locale{"C"}** **指的是"经典"C 语言环境** ;本书通篇都使用此环境。其他**locale**名称则由具体实现定义。 **locale{""}** **被视为"用户的首选语言环境"** 。此语言环境由程序执行环境中的非语言方式设置。因此,要查看你当前的**"首选语言环境"**,请键入: **locale loc("");** **cout \<\< loc.name() \<\< '\\n';** 我的Windows笔记本电脑上显示: **English_United States.1252** 在我的Linux系统上,我得到了: **en_US.UTF** **−8** **C++** **中本地化设置的名称没有统一的标准。相反,诸如** **POSIX** **和** **Microsoft** **等各种组织针对不同的编程语言维护着各自不同的标准。**例如: |---------------------|-----------------------| | GNU本地化名称示例(基于POSIX) || | **ja_JP** | 日本的日语 | | **da_DK** | 丹麦的丹麦语 | | **en_DK** | 丹麦的英语 | | **de_CH** | 瑞士的德语 | | **de_DE** | 德国的德语 | | **en_GB** | 英语的英语 | | **en_US** | 美国的英语 | | **fr_CA** | 加拿大的法语 | | **de_DE@euro** | 德国的具有欧元符号€的德语 | | **de_DE.utf8** | 使用UTF-8的德国的德语 | | **de_DE.utf8@euro** | 使用具有欧元符号€的UTF-8的德国的德语 | POSIX 建议的格式为:小写语言名称,后跟可选的大写国家名称,再后跟可选的编码说明符,例如 sv_FI@euro(瑞典语,芬兰语,包含欧元符号)。 |---------------------------------| | 微软本地化名称示例 | | **Arabic_Qatar.1256** | | **Basque.Spain.1252** | | **Chinese_Singapore.936** | | **English_United Kingdom.1252** | | **English_United States.1252** | | **French_Canada.1252** | | **Greek_Greece.1253** | | **Hebrew_Israel.1255** | | **Hindi_India.1252** | | **Russian_Russia.1251** | 微软使用语言名称,后跟国家/地区名称(可选),再后跟代码页编号。**代码页**(code page)是一种命名(或编号)的字符编码。 大多数操作系统都提供了为程序设置默认本地化的方法。通常,这是通过诸如 LC_ALL、LC_COLLATE 和 LANG 之类的环境变量来实现的。通常,当用户首次接触系统时,会选择一个适合自己的本地化。例如,我预计将 Linux 系统配置为使用阿根廷西班牙语作为默认设置的用户会发现 **locale{""}** 指的是 **locale{"es_AR"}** 。**然而,这些名称在不同平台上并没有统一的标准。因此,要在特定系统上使用命名本地化,程序员必须查阅系统文档并进行实践。** **通常来说,最好避免在程序文本中嵌入本地化名称字符串。** 在程序文本中提及文件名或系统常量会限制程序的可移植性,并且常常迫使想要将程序适配到新环境的程序员去查找并修改这些值。提及区域设置名称字符串也会带来类似的负面影响。**相反,可以从程序的执行环境中获取本地化设置** (例如,使用**locale("")** 或读取文件)。或者,程序可以请求用户通过输入字符串来指定备选本地化设置。例如: **void user_set_locale(const string\& question)** **{** **cout \<\< question; //** e.g., "If you want to use a different locale, please enter its name" **string s;** **cin \>\> s;** **locale::global(locale{s}); //** set global locale as specified by user **}** 通常情况下,最好让非专业用户从一系列选项中进行选择。实现此功能的函数需要知道系统将本地化设置保存在哪里以及如何保存。例如,许多 Linux 系统将本地化设置保存在 /usr/share/locale 目录中。 如果字符串参数未指向已定义的本地化设置,则构造函数会抛出 runtime_error 异常(§30.4.1.1)。例如: **void set_loc(locale\& loc, const char** **∗** **name)** **try** **{** **loc = locale{name};** **}** **catch (runtime_error\&) {** **cerr \<\< "locale** **// ...** **}** 如果某个本地化设置有**名称字符串** ,**name()** 函数将返回该名称字符串。否则,**name()** 函数将返回 **string("\*")** 。**名称字符串主要用于引用存储在执行环境中的本地化设置。**其次,名称字符串还可以用作调试辅助工具。例如: **void print_locale_names(const locale\& my_loc)** **{** **cout \<\< "name of current global locale: " \<\< locale().name() \<\< "\\n";** **cout \<\< "name of classic C locale: " \<\< locale::classic().name() \<\< "\\n";** **cout \<\< "name of** **''user's preferred locale'': " \<\< locale("").name() \<\< "\\n";** **cout \<\< "name of my locale: " \<\< my_loc.name() \<\< "\\n";** **}** #### **39.2.1.1** **构造新的** **locale** **(** Constructing New **locale** s**)** 创建新的**locale** 是通过在现有**locale** 的基础上添加或替换**facet** 来实现的。**通常,新的** **locale** **只是基于现有本地化的一个细微变体。**例如: **void f(const locale\& loc, const My_money_io** **∗** **mio) // My_money_io defined in** **§** **39.4.3.1** **{** **locale loc1(locale{"POSIX"},loc,locale::monetary); // use monetary facets from loc** **locale loc2 = locale(locale::classic(), mio); // classic plus mio** **// ...** **}** 这里,**loc1** 是 **POSIX locale** 的副本,经过修改以使用 **loc** 的货币**facet** (§39.4.3)。类似地,**loc2** 是 C **locale** 的副本,经过修改以使用 **My_money_io** (§39.4.3.1)。最终的**locale**可以表示如下: ![](https://i-blog.csdnimg.cn/direct/a299a204b5624e0ba9077a62e70dfdc5.png) 如果 **Facet\*** 参数(此处为 **My_money_io** )为**nullptr** ,则生成的**locale** 只是**locale**参数的副本。 在构造函数 **locale{loc,f }** 中,参数 **f** 必须标识一个特定的**facet** 类型。普通的 **facet** **∗** 是不够的。例如: **void g(const locale::facet** **∗** **mio1, const money_put\** **∗** **mio2)** **{** **locale loc3 = locale(locale::classic(), mio1); // error : type of facet not known** **locale loc4 = locale(locale::classic(), mio2); // OK: type of facet known (moneyput\)** **// ...** **}** **locale** 使用 **Facet\*** 参数的类型在编译时确定 **facet** 的类型。具体来说,**locale** 的实现使用 **facet** 的标识类型 **facet::id** (§39.3)在**locale** 中查找该 **facet**(§39.3.1)。构造函数 **template\ locale(const locale\& x, Facet** **∗** **f);** 是该语言中唯一允许程序员通过**locale** 提供**facet** 的机制。其他**locale** 由实现者以命名本地化设置的形式提供(§39.2.1)。命名本地化可以从程序的执行环境中获取。了解特定实现机制的程序员或许能够添加新的**locale**。 **locale** 构造函数集的设计使得每一个 **facet** 的类型都可以通过类型推导(根据 **Facet** 模板参数)或来自其他**locale** (已知其类型)来得知。指定 **category** 参数间接指定了 **facet** 的类型,因为**locale** 已知 **category** 中 **facet** 的类型。这意味着**locale** 类可以(并且确实)跟踪 **facet** 的类型,从而能够以最小的开销操作它们。 **locale::id** 成员类型由 **locale** 用于标识 **facet** 类型(§39.3)。 无法修改**locale** 。相反,**locale** 操作提供了从现有本地化创建新**locale** 的方法。**locale** **创建后不可更改,这对于运行时效率至关重要** 。这使得使用**locale** 的用户能够调用 **facet** 的虚函数并缓存返回值。例如,**istream** 可以知道用什么字符来表示小数点,以及 如何在每一次读取一个数时都无需调用**decimal_point()** 、如何在每一次读一个**bool** 值时都无需调用**truename()** (§39.4.2)。只有对流调用**imbue()**(§38.4.5.1)才会导致此类调用返回不同的值。 ### **39.2.2** **比较字符串 (** Comparing **string** s**)** 根据**locale** 比较两个**string** 可能是**locale** 除了 I/O 操作之外最常见的用途。因此,**locale** 直接提供了此操作,用户无需根据**collate** 的**facet** (§39.4.1)构建自己的比较函数。此**string** 比较函数定义为**locale** 的 **operator()()**。例如: **void user(const string s1, const string s2, const locale\& my_locale)** **{** **if (my_locale(s,s2)) { // is s\\& v, const locale\& my_locale)** **{** **sort(v.begin(),v.end()); //sor t using \< to compare elements** **// ...** **sort(v.begin(),v.end(),my_locale); // sor t according to the rules of my_locale** **// ...** **}** 默认情况下,标准库 **sort()** 使用 **\<** 作为实现字符集的数值来确定排序顺序(§32.6,§31.2.2.1)。 ## **39.3** **类** **facet** **(** Class **facet** **)** **locale** 是一组**facet** 的集合。**每一个** **facet** **代表一种具体的文化特征** ,例如**数在输出中的表示方式** (**num_put** )、**日期从输入中读取的方式** (**time_get** )以及**字符在文件中的存储方式** (**codecvt** )。标准库中的**facet**列于第 39.4 节。 用户可以定义新的**facet** ,例如确定如何打印季节名称的**facet**(§39.3.2)。 在程序中,**facet** 表示为继承自 **std::locale::facet** 的类的对象。与其他所有**locale** 功能一样,**facet** 位于 **\**中: **class locale::facet {** **protected:** **explicit facet(size_t refs = 0);** **virtual** **˜facet();** **facet(const facet\&) = delete;** **void operator=(const facet\&) = delete;** **};** **facet** 类被设计为基类,并且没有公共函数。它的构造函数是受保护的,以防止创建"普通 **facet**"对象;它的析构函数是虚函数,以确保派生类对象被正确销毁。 **facet** 旨在通过存储在 **locale** 中的指针进行管理。**facet** 构造函数的参数为​​ 0 表示 **locale** 应在最后一个引用消失时删除该**facet** 。相反,非零构造函数参数确保 **locale** 永远不会删除该**facet** 。非零参数用于极少数情况下 **facet** 的生命周期由程序员直接控制,而不是通过 **locale** 间接控制的情况。 每一种类型的**facet** 接口都必须有单独的 **id**: **class locale::id {** **public:** **id();** **void operator=(const id\&) = delete;** **id(const id\&) = delete;** **};** **id** 的预期用途是供用户定义每一个提供新 **facet** 接口的类的**id** 类型静态成员(例如,参见 §39.4.1)。**locale** 机制使用 **id** 来标识 **facet** (§39.2,§39.3.1)。在**locale** 的直接实现中,**id** 用作 **facet** 指针向量的索引,从而实现高效的 **map\**。 用于定义(派生)**facet** 的数据在派生类中定义。这意味着定义 **facet** 的程序员对数据拥有完全控制权,并且可以使用任意数量的数据来实现**facet** 所表示的概念。 **facet** 的设计初衷是不可变的,因此用户定义的**facet** 的所有成员函数都应该定义为 **const**。 ### **39.3.1** **在** **locale** **中访问** **facets** **(** Accessing **facet** s in a **locale** **)** 可以通过两个模板函数访问**locale** 的各个**facet**: |---------------------------|-----------------------------------------------------------------------------------| | 非成员 **locale** 函数 || | **f=use_facet\(loc)** | **f** 是指向**loc** 中 **facet F** 的引用;如果**loc** 中没有 **facet F** ,则抛出**bad_cast** 异常。 | | **has_facet\(loc)** | **loc** 是否有**facet** **F** **?** noexcept | 可以将这些函数视为在其**locale** 参数中查找模板参数**F** 。或者,可以将 **use_facet** 视为将**locale** 显式转换为特定 **facet** 的一种方式。这是可行的,因为一个**facet** 只能有一个已经类型的**facet**。例如: **void f(const locale\& my_locale)** **{** **char c = use_facet\\>(my_locale).decimal_point() //** use standard facet **//** ... **if (has_facet\(my_locale)) { //** does my_locale contain an Encrypt facet? **const Encrypt\& f = use_facet\(my_locale); //** retrieve Encr ypt facet **const Crypto c = f.g et_crypto(); //** use Encrypt facet **//** ... **}** **//** ... **}** 标准**facet** 保证适用于所有**locale** (§39.4),因此我们不需要对标准**facet** 使用 **has_facet**。 **理解** **facet::id** **机制的一种方式是将其视为一种编译时多态性的优化实现** **。** 可以使用 **dynamic_cast** 来获得与 **use_facet** 非常相似的结果。然而,特化的**use_facet** 实现方式比通用的**dynamic_cast**更高效。 **id** 用于标识接口和行为,而非类。也就是说,如果两个**facet** 类具有完全相同的接口并实现了相同的语义(就**locale** 而言),则它们应该使用相同的 **id** 进行标识。例如,**collat** **e\** 和 **collat** **e_byname\** 在同一**locale** 中可以互换,因此它们都使用 **collat** **e\::id** 进行标识(§39.4.1)。 如果我们定义一个具有新接口的**facet** ( 例如 **f()** 中的 **Encrypt** ),则必须定义一个相应的 **id**来标识它(参见 §39.3.2 和 §39.4.1)。 ### **39.3.2** **一个简单的用户定义** **facet** **(** A Simple User-Defined **facet** **)** 标准库为最关键的文化差异领域(例如字符集和数的 I/O )提供了标准**facet** 。为了将**facet** 机制从广泛使用的类型的复杂性及其伴随的效率问题中剥离出来进行考察,我首先介绍一个简单的用户自定义类型的**facet**: **enum Season { spring, summer, fall, winter }; // very simple user-defined type** 这里概述的 I/O 格式只需稍作修改即可用于大多数简单的用户自定义类型。 **class Season_io : public locale::facet {** **public:** **Season_io(int i = 0) : locale::facet{i} { }** **˜Season_io() { } //** to make it possible to destroy Season_io objects (§39.3) **virtual const string\& to_str(Season x) const = 0; //** string representation of x **virtual bool from_str(const string\& s, Season\& x) const = 0; //** place Season for s in x **static locale::id id; //** facet identifier object (§39.2, §39.3, §39.3.1) **};** **locale::id Season_io::id; //** define the identifier object 为简单起见,此**facet** 仅限于**char** 型**string**。 **Season_io** 类为所有 **Season_io** 文化特征提供了一个通用的抽象接口。要定义特定**locale** 环境下的 **Season** 的 I/O 表示,我们需要从**Season_io** 类派生出一个类,并相应地定义**to_str()** 和**from_str()**函数。 输出**Season** 信息很简单。如果数据流包含 **Season_io** 的文化特征,我们可以使用它把值转换成字符串。否则,我们可以直接输出**Season** 的**int**值: **ostream\& operator\<\<(ostream\& os, Season x)** **{** **locale loc {os.getloc()}; // extract the stream** **'** **s locale (** **§** **38.4.4)** **if (has_facet\(loc))** **return os \<\< use_facet\(loc).to_str(x); // string representation** **return os \<\< static_cast\(x); // integer representation** **}** 为了最大限度地提高效率和灵活性,标准**facet** 通常直接操作流缓冲区(§39.4.2.2,§39.4.2.3)。但是,对于像**Season** 这样简单的用户自定义类型,则无需降低抽象的**streambuf**级别。 通常情况下,输入比输出要复杂一些: **istream\& operator\>\>(istream\& is, Season\& x)** **{** **const locale\& loc {is.getloc()}; // extract the stream** **'** **s locale (** **§** **38.4.4)** **if (has_facet\(loc)) {** **const Season_io\& f {use_facet\(loc)}; // get hold of the locale** **'** **s Season_io facet** **string buf;** **if (!(is\>\>buf \&\& f.from_str(buf,x))) // read alphabetic representation** **is.setstate(ios_base::failbit);** **return is;** **}** **int i;** **is \>\> i; // read numer ic representation** **x = static_cast\(i);** **return is;** **}** 错误处理很简单,遵循内置类型的错误处理风格。也就是说,如果输入字符串在所选本地化中不代表**Season** ,则流将进入失败状态。如果启用了异常处理,则意味着会抛出 **\`ios_base::failure\`**异常(参见第 38.3 节)。 这是一个简单的测试程序: **int main()** **// a trivial test** **{** **Season x;** **// use the default locale (no Season_io facet) implies integer I/O:** **cin \>\> x;** **cout \<\< x \<\< endl;** **locale loc(locale(),new US_season_io{});** **cout.imbue(loc); // use locale with Season_io facet** **cin.imbue(loc); // use locale with Season_io facet** **cin \>\> x;** **cout \<\< x \<\< endl;** **}** 已知输入: **2** **summer** 程序响应: **2** **summer** 为此,我们必须从**Season_io** 派生出一个 **US_season_io**类,并定义一个合适的季节字符串表示形式: **class US_season_io : public Season_io {** **static const string seasons\[\];** **public:** **const string\& to_str(Season) const;** **bool from_str(const string\&, Season\&) const;** **// note: no US_season_io::id** **};** **const string US_season_io::seasons\[\] = {** **"spring",** **"summer",** **"fall",** **"winter"** **};** 然后,我们重写 **Season_io**函数,这些函数用于在字符串表示形式和枚举器之间进行转换: **const string\& US_season_io::to_str(Season x) const** **{** **if (x\\>** 运算符负责,而对于输出,**facet** 函数**to_str()** 实现了错误处理策略。这样做是为了说明不同的设计方案。在"生产环境设计"中,**facet** 函数要么同时实现输入和输出的错误处理,要么只报告错误信息,以便**\>\>** 和**\<\<**运算符进行处理。 此 **Season_io** 设计依赖于派生类来提供特定本地化的字符串。另一种设计方案是让 **Season_io** 本身从特定于本地化的存储库中检索这些字符串(参见 §39.4.7)。至于是否可以只使用一个**Season_io**类,并将季节字符串作为构造函数参数传递给它,则留待后续研究。 ### **39.3.3** **使用** **locale** **和** **facet** **(** Uses of **locale** s and **facet** s**)** **标准库中本地化的主要用途是用于 I/O 流中** 。然而,本地化机制是一种通用且可扩展的机制,**用于表示文化敏感信息** 。**messages** 文化特征(§39.4.7)就是一个与 I/O 流无关的 **facet** 示例。**iostream** 库的扩展,甚至非基于流的 I/O 功能,都可能利用本地化设置。此外,用户还可以使用本地化设置来方便地组织任意的文化敏感信息。 由于**locale/facet** 机制的通用性,用户自定义**facet** 的可能性是无限的。可以表示为**facet**的内容包括日期、时区、电话号码、社会保障号码(个人识别号码)、产品代码、温度、通用(单位,值)对、邮政编码、服装尺码和国际标准书号 (ISBN)。 与其他强大的机制一样,使用**facet** 也应谨慎。能够用**facet**来表示的事物并不意味着它就是最佳的表示方式。在选择文化依赖关系的表示方法时,需要考虑的关键问题------一如既往------是各种决策如何影响编码难度、最终代码的可读性、最终程序的可维护性,以及最终 I/O 操作在时间和空间上的效率。 ## **39.4** **标准** **facet** **(** Standard **facet** s**)** 在 **\** 中,标准库提供了以下**facet**: |----------|----------|----------------------------------------------------------------------------------------------------|---------| | 标准facet (§iso.22.3.1.1.1) |||| | collate | 字符串比较 | **collate\** | §39.4.1 | | numeric | 数值格式化 | **numpunct\** **num_get\** **num_put\** | §39.4.2 | | monetary | 货币格式化 | **moneypunct\** **moneypunct\** **money_get\** **money_put\** | §39.4.3 | | time | 日期和时间格式化 | **time_put\** **time_put_byname\** **time_get\** | §39.4.4 | | ctype | 字符分类 | **ctype\** **codecvt\** **codecvt_byname\** | §39.4.5 | | messages | 消息检索 | **messages\** | §39.4.7 | 详情请参见相关小节。 从该表中实例化 **facet** 时,**C** 必须是字符类型(§36.1)。这些**facet** 保证为 **char** 或 **wchar_t** 类型定义。此外,**ctype\** 保证支持**char16_t** 和 **char32_t** 类型。如果需要使用标准 I/O 处理其他字符类型**X** ,则必须依赖特定于实现的**facet** 特化,或为 **X** 提供合适的**facet** 版本。例如,可能需要 **codecvt\** (§39.4.6)来控制 **X** 和 **char**之间的转换。 **International** 可以是**true** 或**false** ;**true** 表示使用货币符号的三字符(加上零终止符)"international"表示(§39.4.3.1),例如 **USD** 和 **BRL**。 移位状态参数**SS** 用于表示多字节字符表示的移位状态(§39.4.6)。在**\** 中,**mbstate_t** 定义为,表示可以出现在一个由实现所定义的支持多字节字符编码规则集中的任何转换状态。对于任意字符类型**X** ,**mbstate_t** 的等效项是**char_traits\::state_type**(§36.2.2)。 **In** 和**Out** 分别是输入迭代器和输出迭代器(§33.1.2,§33.1.4)。通过这些模板参数为**_put** 和 **_get** 语言特征提供参数,程序员可以提供访问非标准缓冲区的 **facet** (§39.4.2.2)。与**iostream** 关​​联的缓冲区是流缓冲区,因此为它们提供的迭代器是 **ostreambuf_iterators** (§38.6.3,§39.4.2.2)。因此,可以使用函数 **failed()** 进行错误处理(§38.6.3)。 每个标准**facet** 都有一个**_byname** 版本。**F_byname** **facet** 派生自**facet** **F** 。**F_byname** 提供与**F** 相同的接口,但它添加了一个构造函数,该构造函数接受一个字符串参数来命名**locale** (例如,参见 §39.4.1)。**F_byname(name)** 为**locale(name)** 中定义的**F**提供相应的语义。例如: **sort(v.begin(),v.end(),collate_byname{"da_DK"}); //** sort using character comparison from "da_DK" 其思路是从程序执行环境中指定的一个命名**locale** (§39.2.1)中选择一个标准 **facet** 的版本。这意味着,与无需查询环境的构造函数相比,**_byname** 构造函数速度非常慢。构造一个**locale** ,然后访问其 **facet** ,几乎总是比在程序中多处使用**_byname** **facet** 更快。因此,通常的做法是从环境中读取一次 **facet**,然后重复使用主内存中的副本。例如: **locale dk {"da_DK"}; // read the Danish locale (including all of its facets) once** **// then use the dk locale and its facets as needed** **void f(vector\\& v, const locale\& loc)** **{** **const collate\\& col {use_facet\\>(dk)};** **const ctype\\& ctyp {use_facet\\>(dk)};** **locale dk1 {loc,\&col}; // use Danish string comparison** **locale dk2 {dk1,\&ctyp}; // use Danish character classification and string comparison** **sort(v.begin(),v.end(),dk2);** **// ...** **}** 此**dk2**本地化将使用丹麦语风格的字符串,但会保留数的默认约定。 类别的概念为操作**locale** 中的标准**facet** 提供了一种更简单的方法。例如,给定 **dk** 本地化设置,我们可以构建一个**locale**,该本地化设置按照丹麦语(比英语多三个元音)的规则读取和比较字符串,但保留 C++ 中使用数的语法: **locale dk_us(locale::classic(),dk,collate\|ctype); //** **丹麦字母** **,** **美国数** 对各个标准**facet** 的介绍包含了更多**facet** 使用的示例。特别是,对**collate** 的讨论(§39.4.1)揭示了**facet**的许多常见结构特征。 标准**facet** 通常相互依赖。例如,**num_put** 依赖于**numpunct** 。只有深入了解各个 **facet** ,才能成功地混合搭配不同的 **facet** ,或者添加标准 **facet** 的新版本。换句话说,除了简单的操作(例如对 **iostream** 的 **imbue()** 和对 **sort()** 的 **collat** **e** ),**locale**机制并不适合新手直接使用。有关本地化设置的详细讨论,请参阅 \[Langer,2000\]。 单个语言特征的设计通常比较复杂。部分原因是这些语言特征必须反映库设计者无法控制的复杂文化习俗,部分原因是 C++ 标准库的功能必须与 C 标准库和各种平台特定标准的功能保持高度兼容。 **在另一方面,** **locale** **和** **facet** **提供的框架具有通用性和灵活性。** **facet** 可以设计成承载任何数据,并且**facet** 的操作可以根据这些数据提供任何所需的操作。如果新**facet**的行为没有受到过多的约定限制,那么它的设计可以做到简洁明了(§39.3.2)。 ### **39.4.1** **string** 比较**(** **string** Comparison**)** 标准**collate**语言特征提供了比较字符数组的方法: **template\** **class collate : public locale::facet {** **public:** **using char_type = C;** **using string_type = basic_string\;** **explicit collate(size_t = 0);** **int compare(const C** **∗** **b, const C** **∗** **e, const C** **∗** **b2, const C** **∗** **e2) const** **{ return do_compare(b,e,b2,e2); }** **long hash(const C** **∗** **b, const C** **∗** **e) const** **{ return do_hash(b,e); }** **string_type transform(const C** **∗** **b, const C** **∗** **e) const** **{ return do_transform(b,e); }** **static locale::id id; // facet identifier object (** **§** **39.2,** **§** **39.3,** **§** **39.3.1)** **protected:** **˜collate(); // note: protected destructor** **virtual int do_compare(const C** **∗** **b, const C** **∗** **e, const C** **∗** **b2, const C** **∗** **e2) const;** **virtual string_type do_transform(const C** **∗** **b, const C** **∗** **e) const;** **virtual long do_hash(const C** **∗** **b, const C** **∗** **e) const;** **};** 这定义了两个接口: • 面向 **facet** 用户的**public**接口。 • 面向派生 **facet** 实现者的**protected**接口。 构造函数参数指定是由**locale** 还是用户负责删除 **facet** 。默认值**(0)** 表示"让**locale**负责管理"(§39.3)。 **所有标准库** **facet** **都共享一个共同的结构** ,因此关于一个**facet**的主要事实可以通过关键函数来概括: |--------------------------------------------------------------------------------------------------------------| | **collate\ facet** **(§iso.22.4.4.1)** | | **int compare(const C** **∗** **b, const C** **∗** **e, const C** **∗** **b2, const C** **∗** **e2) const;** | | **long hash(const C** **∗** **b, const C** **∗** **e) const;** | | **string_type transform(const C** **∗** **b, const C** **∗** **e) const;** | 要定义一个**facet** ,请使用 **collat** **e** 作为模式。要从标准模式派生,只需定义提供该**facet** 功能的关键函数的**do_** **∗** 版本即可。为了提供足够的信息来编写重载的 **do_** **∗** 函数,我们列出了函数的完整声明(而不是使用模式)。例如,请参见 §39.4.1.1。 **hash()** **函数会计算其输入字符串的哈希值** 。**显然,这对于构建哈希表非常有用**。 **transform()** 函数生成一个字符串,该字符串与另一个经过 **transform()**转换的字符串进行比较,结果与直接比较这两个字符串的结果相同。即: **cf.compare(cf.transform(s),cf.transform(s2)) == cf.compare(s,s2)** **transform()**函数的目的是优化将一个字符串与多个其他字符串进行比较的代码。这在实现字符串集合中的搜索时非常有用。 **compare()** 函数根据特定**collate**定义的规则执行基本的字符串比较。它返回: **1**------ 如果第一个字符串的字典序大于第二个字符串。 **0**------ 如果两个字符串相同。 **-1**------ 如果第二个字符串大于第一个字符串。 例如: **void f(const string\& s1, const string\& s2, const collate\\& cmp)** **{** **const char** **∗** **cs1 {s1.data()}; // because compare() operates on char\[\]s** **const char** **∗** **cs2 {s2.data()};** **switch (cmp.compare(cs1,cs1+s1.siz e(),cs2,cs2+s2.size()) {** **case 0: // identical strings according to cmp** **// ...** **break;** **case** **−1: // s1 \< s2** **// ...** **break;** **case 1: // s1 \> s2** **// ...** **break;** **}** **}** **collate** **成员函数比较的是** **\[b:e)** **范围内的** **C** **字符,而不是** **basic_string** **或以0结尾的** **C** **风格字符串。特别地,数值为** **0** **的** **C** **字符会被视为普通字符,而不是终止符**。 **标准库** **string** **不是** **locale** **敏感的。即,它根据实现所用字符集的规则比较字符串(§6.2.3)** 。**此外,标准** **string** **没有提供直接指定比较标准的方法(第36章)** 。要进行区分**locale** 比较,我们可以使用**collate** 的 **compare()** 函数。例如: **void f(const string\& s1, const string\& s2, const string\& name)** **{** **bool b {s1==s2}; // compare using implementation** **'** **s character set values** **const char** **∗** **s1b {s1.data()}; // get start of data** **const char** **∗** **s1e {s1.data()+s1.size()} // get end of data** **const char** **∗** **s2b {s2.data()};** **const char** **∗** **s2e {s2.data()+s2.size()}** **using Col = collate\;** **const Col\& global {use_facet\(locale{})}; // from the current global locale** **int i0 {global.compare(s1b,s1e,s2b,s2e)};** **const Col\& my_coll {use_facet\(locale{""})}; // from my preferred locale** **int i1 {my_coll.compare(s1b,s1e ,s2b,s2e)};** **const Col\& n_coll {use_facet\(locale{name})}; // from a named locale** **int i2 {n_coll.compare(s1b,s1e,s2b,s2e)};** **}** 从符号学角度来看,通过**locale** 的**operator()** 函数间接使用 **collat** **e** 的 **compare()** 函数可能更方便(§39.2.2)。例如: **void f(const string\& s1, const string\& s2, const string\& name)** **{** **int i0 = locale{}(s1,s2); //** **使用当前全局** **locale** **进行比较** **int i1 = locale{""}(s1,s2); //** **使用我喜欢的** **locale** **进行比较** **int i2 = locale{name}(s1,s2); //** **使用命名** **locale** **进行比较** **// ...** **}** 不难想象 **i0** 、**i1** 和 **i2**不同的情况。请看以下这段来自德语词典的单词序列: **Dialekt, Di** **¨ at, dich, dichten, Dichtung** 按照惯例,名词(仅名词)首字母大写,但字母顺序不区分大小写。 区分大小写的德语排序会将所有以 **D** 开头的单词排在 **d** 之前: **Dialekt, Di** **¨ at, Dichtung, dich, dichten** **ä** ( 元音变音**a** ) 被视为"一种 **a** ",因此它排在 **c** 之前。然而,在大多数常用字符集中,**ä** 的数值大于 **c** 的数值。因此,**int('c')\** **class collate_byname : public collate\ { // note: no id and no new functions** **public:** **typedef basic_string\ string_type;** **explicit collate_byname(const char** **∗** **, siz e_t r = 0); // construct from named locale** **explicit collate_byname(const string\&, size_t r = 0);** **protected:** **˜collate_byname(); // note: protected destructor** **int do_compare(const C** **∗** **b, const C** **∗** **e, const C** **∗** **b2, const C** **∗** **e2) const override;** **string_type do_transform(const C** **∗** **b, const C** **∗** **e) const override;** **long do_hash(const C** **∗** **b, const C** **∗** **e) const override;** **};** 因此,可以使用 **collat** **e_byname** 从程序执行环境中的一个命名**locale** 中选取一个**collat** **e** (§39.4)。在执行环境中存储 **facet** 的一种显而易见的方法是将其作为数据存储在文件中。另一种灵活性较低的方法是将 **facet** 表示为程序文本和一个 **_byname** 语言特征中的数据。 ### **39.4.2** **数值格式化(** NumericFormatting**)** 数值输出由 **num_put** 语言特征写入流缓冲区完成(§38.6)。相反,数值输入由 **num_get** 语言特征从流缓冲区读取完成。**num_put** 和 **num_get** 使用的格式由名为 **numpunct** 的"数值标点" **facet** 定义。 #### **39.4.2.1** **数值标点(** Numeric Punctuation**)** **numpunct** 语言特征定义了内置类型(例如 **bool** ,**int** 和 **double**)的 I/O 格式: |------------------------------------|---------------------| | **numpunct\ facet** **(§iso.22.4.6.3.1)** || | **C decimal_point() const;** | 例如,**'.'** | | **C thousands_sep() const;** | 例如,**','** | | **string grouping() const;** | 例如,**""**, 指的是"不分组" | | **string_type truename() const;** | 例如,**"true"** | | **string_type falsename() const;** | 例如,**"false"** | **grouping()** 函数返回的字符串字符被读取为一系列小的整数值。每个数指定一个分组的位数。字符**0** 指定最右边的分组(最低有效位),字符 **1** 指定其左侧的分组,依此类推。因此,**"\\004\\002\\003"** 表示像**123-45-6789** 这样的数(前提是使用"**-** "作为分隔符)。如有必要,分组模式中的最后一个数可以重复使用,因此**"\\003"** 等效于**"\\003\\003\\003"** 。分组最常见的用途是使大数更易于阅读。**grouping()** 和 **thousands_sep()** 函数定义了整数和浮点数整数部分的输入和输出格式。 我们可以从**numpunct** 派生出一种新的标点符号样式。例如,我可以定义语言特征 **My_punct** 来写入整数值,用空格将数分组为三位一组,并写入浮点值,使用欧式逗号作为"小数点": **class My_punct : public numpunct\ {** **public:** **explicit My_punct(size_t r = 0) :numpunct\(r) { }** **protected:** **char do_decimal_point() const override { return ','; } // comma** **char do_thousands_sep() const override { return '_'; } // underscore** **string do_grouping() const override { return "\\003"; } // 3-digit groups** **};** **void f()** **{** **cout \<\< "style A: " \<\< 12345678** **\<\< "** **∗∗∗** **" \<\< 1234567.8** **\<\< "** **∗∗∗** **" \<\< fixed \<\< 1234567.8 \<\< '\\n';** **cout \<\< defaultfloat; // reset floating for mat** **locale loc(locale(),new My_punct);** **cout.imbue(loc);** **cout \<\< "style B: " \<\< 12345678** **\<\< "** **∗∗∗** **" \<\< 1234567.8** **\<\< "** **∗∗∗** **" \<\< fixed \<\< 1234567.8 \<\< '\\n';** **}** 这产生: **style A: 12345678** **∗∗∗** **1.23457e+06** **∗∗∗** **1234567.800000** **style B: 12_345_678** **∗∗∗** **1_234_567,800000** **∗∗∗** **1_234_567,800000** 请注意,**imbue()** 函数会将参数的副本存储在其流中。因此,即使原**locale** 副本已被销毁,流仍然可以依赖于已注入的**locale** 。如果 **iostream** 的 **boolalpha** 标志已设置(参见 §38.4.5.1),则**truename()** 和 **falsename()** 返回的字符串分别用于表示**true** 和**false** ;否则,分别使用 **1** 和**0**。 标准库提供了 **numpunct** 的一个**_byname**版本(§39.4,§39.4.1): **template\** **class numpunct_byname : public numpunct\ {** **// ...** **};** #### **39.4.2.2** **数值输出(** Numeric Output**)** 当写入流缓冲区时(§38.6),**ostream** 依赖于 **num_put**语言特征: |--------------------------------------------------------------------------------------------------------| | **num_put\\> facet** **(§iso.22.4.2.2)** 将值**v** 放入数据流**s** 中缓冲区位置**b** | | **Out put(Out b, ios_base\& s, C fill, bool v) const;** | | **Out put(Out b, ios_base\& s, C fill, long v) const;** | | **Out put(Out b, ios_base\& s, C fill, long long v) const;** | | **Out put(Out b, ios_base\& s, C fill, unsigned long v) const;** | | **Out put(Out b, ios_base\& s, C fill, unsigned long long v) const;** | | **Out put(Out b, ios_base\& s, C fill, double v) const;** | | **Out put(Out b, ios_base\& s, C fill, long double v) const;** | | **Out put(Out b, ios_base\& s, C fill, const void** **∗** **v) const;** | **put()**的值是指向写入的最后一个字符位置之后一位的迭代器。 **num_put** 的默认特化(用于访问字符的迭代器类型为 **ostreambuf_iterator\** )是标准**locale** 的一部分(§39.4)。要在其他地方使用**num_put** 写入数据,我们必须定义一个合适的特化。例如,以下是一个非常简单的用于写入字符串的**num_put**示例: **template\** **class String_numput : public num_put\::iterator\> {** **public:** **String_numput() :num_put\::iterator\>{1} { }** **};** 我并不打算让 **String_numput** 进入**locale**,所以我使用了构造函数参数来保留其常规的生命周期规则。预期用法大致如下: **void f(int i, string\& s, int pos) // format i into s starting at pos** **{** **String_numput\ f;** **f.put(s.begin()+pos,cout,' ',i); // format i into s; use cout** **'** **s for matting rules** **}** **ios_base** 参数(此处为 **cout** )提供有关格式化状态和**locale**的信息。例如: **void test(iostream\& io)** **{** **locale loc = io.getloc();** **wchar_t wc = use_facet\\>(loc).widen(c); // char to C conversion** **string s = use_facet\\>(loc).decimal_point(); // default:** **'** **.** **'** **string false_name = use_facet\\>(loc).falsename(); // default: "false"** **}** 像 **num_put\** 这样的标准 **facet** 通常通过标准的 I/O 流函数隐式使用。因此,大多数程序员无需了解它。然而,标准库函数对这些 **facet** 的使用方式很有意思,因为它们展示了 I/O 流的工作原理以及如何使用 **facet**。一如既往,标准库提供了许多有趣的编程技巧示例。 使用 **num_put** ,**ostream**的实现者可能会这样写: **template\** **basic_ostream\\& basic_ostream\::operator\<\<(double d)** **{** **sentry guard(** **∗** **this); // see** **§** **38.4.1** **if (!guard) return** **∗** **this;** **try {** **if (use_facet\\>(g etloc()).put(** **∗** **this,** **∗** **this,this** **−\>fill(),d).failed())** **setstate(badbit);** **}** **catch (...) {** **handle_ioexception(** **∗** **this);** **}** **return** **∗** **this;** **}** 这里涉及很多内容。哨兵机制确保所有前缀和后缀操作都已执行(§38.4.1)。我们通过调用其成员函数 **getloc()** 获取 **ostream** 的**locale** (§38.4.5.1)。我们使用 **use_facet** 从该**locale** 中提取 **num_put** 值(§39.3.1)。完成这些步骤后,我们调用相应的 **put()** 函数来执行实际操作。**ostreambuf_iterator** 可以从 **ostream** 构造(§38.6.3),并且 **ostream** 可以隐式转换为其基类**ios_base** (§38.4.4),因此 **put()**的前两个参数很容易提供。 调用 **put()** 函数会返回其输出迭代器参数。该输出迭代器取自 **basic_ostream** ,因此它是一个 **ostreambuf_iterator** 。因此,我们可以使用 **failed()**函数(参见 §38.6.3)来检测操作是否失败,并允许我们相应地设置流状态。 我没有使用 **has_facet** ,因为标准 **facet** (§39.4)保证存在于每个**locale** 中。如果违反此保证,则会抛出**bad_cast**异常(§39.3.1)。 **put()** 函数会调用虚函数 **do_put()** 。因此,可能会执行用户自定义代码,并且 **operator\<\<()** 必须准备好处理重载 **do_put()** 函数时抛出的异常。此外,某些字符类型可能不存在**num_put** 函数,因此 **use_facet()** 可能会抛出 **bad_cast** 异常(§39.3.1)。对于内置类型(例如 **double** ),**\<\<** 的行为由 C++ 标准定义。因此,问题不在于 **handle_ioexception()** 应该做什么,而在于它应该如何执行标准规定的操作。如果此**ostream** 的异常状态中设置了**badbit** (§38.3),则异常会被重新抛出。否则,异常的处理方式是设置流状态并继续执行。无论哪种情况,都必须在流状态中设置 **badbit**(§38.4.5.1): **template\** **void handle_ioexception(basic_ostream\\& s)// called from catch-clause** **{** **if (s.exceptions()\&ios_base::badbit) {** **try {** **s.setstate(ios_base::badbit); // might throw basic_ios::failure** **}** **catch(...) {** **// ... do nothing ...** **}** **throw; // re-throw** **}** **s.setstate(ios_base::badbit);** **}** 需要使用 **try** 代码块,因为 **setstate()** 可能会抛出 **basic_ios::failure** 异常(§38.3,§38.4.5.1)。但是,如果在异常状态下设置了**badbit** ,则 **operator\<\<()** 必须重新抛出导致调用 **handle_ioexception()** 的异常(而不是简单地抛出 **basic_ios::failure**异常)。 对于内置类型(例如 **double** ),**\<\<** 必须通过直接写入流缓冲区来实现。而对于用户自定义类型的 **\<\<**,我们通常可以通过将用户自定义类型的输出用现有类型的输出来表示(§39.3.2),从而避免由此产生的复杂性。 #### **39.4.2.3** **数值输入(** Numeric Input**)** 从流缓冲区读取数据时(§38.6),**istream** 依赖于 **num_get** 语言特征: |-----------------------------------------------------------------------------------------------------------------------------------| | **num_g et\\> facet** **(§iso.22.4.2.1)** 将**\[b:e)** 读取到 **v** 中,使用来自**s** 的格式化规则,并通过设置 **r**报告错误。 | | **In get(In b, In e, ios_base\& s, ios_base::iostate\& r, bool\& v) const;** | | **In get(In b, In e, ios_base\& s, ios_base::iostate\& r, long\& v) const;** | | **In get(In b, In e, ios_base\& s, ios_base::iostate\& r, long long\& v) const;** | | **In get(In b, In e, ios_base\& s, ios_base::iostate\& r, unsigned short\& v) const;** | | **In get(In b, In e, ios_base\& s, ios_base::iostate\& r, unsigned int\& v) const;** | | **In get(In b, In e, ios_base\& s, ios_base::iostate\& r, unsigned long\& v) const;** | | **In get(In b, In e, ios_base\& s, ios_base::iostate\& r, unsigned long long\& v) const;** | | **In get(In b, In e, ios_base\& s, ios_base::iostate\& r, float\& v) const;** | | **In get(In b, In e, ios_base\& s, ios_base::iostate\& r, double\& v) const;** | | **In get(In b, In e, ios_base\& s, ios_base::iostate\& r, long double\& v) const;** | | **In get(In b, In e, ios_base\& s, ios_base::iostate\& r, void** **∗** **\& v) const;** | 基本上,**num_get** 的组织结构与 **num_put** 类似(参见 §39.4.2.2)。由于它执行的是读取操作而非写入操作,因此**get()**需要一对输入迭代器,并且指定读取目标的参数是一个引用。 **iostate** 变量**r** 用于反映流的状态。如果无法读取所需类型的值,则**r** 中的 **failbit** 会设置;如果触及输入结束符,则 **r** 中的 **eofbit** 会设置。输入运算符将使用 **r** 来确定如何设置其流的状态。如果没有遇到错误,则读取的值将赋给 **v** ;否则,**v**保持不变。 哨兵机制用于确保流的前缀和后缀操作得到执行(§38.4.1)。具体来说,哨兵机制用于确保我们仅在流处于良好状态时才尝试读取数据。例如,**istream** 的实现者可能会这样写: **template\** **basic_istream\\& basic_istream\::operator\>\>(double\& d)** **{** **sentry guard(** **∗** **this); // see** **§** **38.4.1** **if (!guard) return** **∗** **this;** **iostate state = 0; // good** **istreambuf_iterator\ eos;** **try {** **double dd;** **use_facet\\>(g etloc()).get(** **∗** **this,eos,** **∗** **this,state,dd);** **if (state==0 \|\| state==eofbit) d = dd; // set value only if get() succeeded** **setstate(state);** **}** **catch (...) {** **handle_ioexception(** **∗** **this); // see** **§** **39.4.2.2** **}** **return** **∗** **this;** **}** 我已注意,除非读取操作成功,否则不会修改 **\>\>**的目标。遗憾的是,这无法保证所有输入操作都能成功。 如果发生错误,则为**istream** 启用的异常将由 **setstate()**抛出(§38.3)。 通过定义一个**numpunct** (例如 §39.4.2.1 中的 **My_punct**),我们可以使用非标准标点符号进行阅读。例如: **void f()** **{** **cout \<\< "style A: "** **int i1;** **double d1;** **cin \>\> i1 \>\> d1; //** read using standard ''12345678'' for mat **locale loc(locale::classic(),new My_punct);** **cin.imbue(loc);** **cout \<\< "style B: "** **int i2;** **double d2;** **cin \>\> i1 \>\> d2; //** read using the ''12_345_678'' for mat **}** 如果我们想要读取非常特殊的数字格式,就必须**重写** **do_get()** 函数。例如,我们可以定义一个 **num_get**函数来读取罗马数字,例如 XXI 和 MM 。 ### **39.4.3** **货币格式化(** Money Formatting**)** 币值的格式在技术上与"普通"数的格式类似(§39.4.2)。然而,币值的呈现方式对文化差异更为敏感。例如,负数(损失、借方),如 -1.25,在某些情况下应以括号括起来的(正)数表示:(1.25)。同样,在某些情况下,会使用颜色来帮助识别负数。 **没有标准的"货币类型"** 。**相反,货币属性旨在明确地用于程序员知道代表货币金额的数值。**例如: **struct Money { // simple type to hold a monetary amount** **using Value = long long; // for currencies that have suffered inflation** **Value amount;** **};** **// ...** **void f(long int i)** **{** **cout \<\< "value= " \<\< i \<\< " amount= " \<\< Money{i} \<\< '\\n';** **}** 货币方面的任务是使编写 **Money** 的输出运算符变得相对容易,以便根据本地约定打印金额(参见 §39.4.3.2)。输出将根据 **cout** 的**locale**而变化。可能的输出包括: **value= 1234567 amount= $12345.67** **value= 1234567 amount= 12345,67 DKK** **value= 1234567 amount= CAD 12345,67** **value=** **−1234567 amount= $−12345.67** **value=** **−1234567 amount= −€12345.67** **value=** **−1234567 amount= (CHF12345,67)** 对于货币而言,精确到最小货币单位通常至关重要。因此,我采用了常见的约定,即用整数值表示分(便士、厄尔、菲尔、分等)的数量,而不是美元(英镑、克朗、第纳尔、欧元等)的数量。**moneypunct** 的 **frac_digits()** 函数(§39.4.3.1)支持此约定。同样,"小数点"的显示方式由**decimal_point()**函数定义。 **money_get** 和**money_put** 这两个**facet** 提供了根据 **money_base**语言特征定义的格式执行 I/O 的函数。 简单的**Money** 类型可用于控制 I/O 格式或存储货币值。在前一种情况下,我们会在写入之前将用于存储货币值的(其他)类型的值强制转换为**Money** 类型;在将值读取到 **Money** 变量之前,我们会先将其转换为其他类型。始终使用 **Money** 类型存储货币值可以降低出错的概率;这样,我们就不会忘记在写入之前将值强制转换为 **Money** 类型,也不会因为尝试以不区分**locale** 的方式读取货币值而导致输入错误。然而,在并非为此设计的系统中引入 **Money** 类型可能并不可行。在这种情况下,必须对读写操作应用 **Money** 类型转换(强制转换)。 #### **39.4.3.1** **货币标点(** Money Punctuation**)** 控制货币金额呈现的**facet** 的**moneypunct** 自然类似于控制普通数的**facet** 的 **numpunct**(§39.4.2.1): **class money_base {** **public:** **enum part { // par ts of value layout** **none, space , symbol, sign, value** **};** **struct pattern { // layout specification** **char field\[4\];** **};** **};** **template\** **class moneypunct : public locale::facet, public money_base {** **public:** **using char_type = C;** **using string_type = basic_string\;** **// ...** **};** **moneypunct**成员函数定义了货币输入和输出的布局: |---------------------------------------------|------------------------------------------------------------| | **moneypunct\\> facet** **(§iso.22.4.6.3)** || | **C decimal_point() const;** | 例如,**'.'** | | **C thousands_sep() const;** | 例如,**','** | | **string grouping() const;** | 例如,**""** 指的是"不分组" | | **string_type curr_symbol() const;** | 例如,**"$"** | | **string_type positive_sign() const;** | 例如,**""** | | **string_type negative_sign() const;** | 例如,**"** **−"** | | **int frac_digits() const;** | **"."** 之后的数字数量,例如,**2** | | **pattern pos_format() const;** | **symbol** , **space** , **sign** , **none** , 或 **value** | | **pattern neg_format() const;** | **symbol** , **space** , **sign** , **none** , 或 **value** | | **static const bool intl = International;** | 使用三个字母的国际缩写 | **moneypunct** 提供的设施主要供 **money_put** 和**money_get** 语言特征(§39.4.3.2,§39.4.3.3)的实施者使用。 **moneypunct** 的**_byname** 版本(§39.4,§39.4.1)如下: **template\** **class moneypunct_byname : public moneypunct\ {** **// ...** **};** **decimal_point()** 、**thousands_sep()** 和 **grouping()** 成员的行为与**numpunct** 中的行为相同。 **curr_symbol()** 、**positive_sign()** 和 **negative_sign()** 成员分别返回用于表示货币符号(例如 **$** 、**¥** 、**INR** 、**DKK** )、加号和减号的字符串。如果 **International** 模板参数为**true** ,则**intl** 成员也为**true** ,并且将使用货币符号的"国际"表示形式。这种"国际"表示形式是一个四字符的 C 风格字符串。例如: **"USD"** **"DKK"** **"EUR"** 最后一个(不可见)字符是终止符零。三字母货币标识符由 ISO-4217 标准定义。当**International** 设置为**false** 时,可以使用"本地"货币符号,例如**$** 、**£** 和 **¥** 。 pos_format() 或 neg_format() 返回的模式由四部分组成,分别定义了数值、货币符号、正负号和空格出现的顺序。大多数常见格式都可以用这种简单的模式概念轻松表示。例如: **+$ 123.45 // { sign, symbol, space, value } where positive_sign() returns "+"** **$+123.45 // { symbol, sign, value, none } where positive_sign() returns "+"** **$123.45 // { symbol, sign, value, none } where positive_sign() returns ""** **$123.45** **− //{ symbol, value, sign, none }** **−123.45 DKK // { sign, value, space, symbol }** **($123.45) // { sign, symbol, value, none } where negative_sign() returns "()"** **(123.45DKK) // { sign, value, symbol, none } where negative_sign() returns "()"** 使用括号表示负数的方法是让 **next_sign()** 函数返回一个包含两个字符 **()** 的字符串。符号字符串的第一个字符位于模式中**sign** 出现的位置,符号字符串的其余部分则位于模式中所有其他部分之后。此功能最常见的用途是表示金融界使用括号表示负数的惯例,但它还有其他用途。例如: **−$123.45 //{ sign, symbol, value, none } where negative_sign() returns "** **−** **"** **∗** **$123.45 silly // { sign, symbol, value, none } where negative_sign() returns "\* silly"** **sign** 、**value** 和 **symbol** 这三个值在模式中必须各出现一次。剩余的值可以是 **space** 或**none** 。在 **space** 出现的地方,则表示中至少可以出现一个也可能出现多个空格字符。在**none** 出现的地方,在表示中可以出现一个或多个空白字符(模式末尾除外)。 请注意,这些严格的规则禁止了一些看似合理的模式: **pattern pat = { sign, value , none , none }; // error : no symbol** **frac_digits()** 函数标识小数点的位置。通常,货币金额以最小货币单位表示(§39.4.3)。该单位通常是主要单位的百分之一(例如,¢ 是 $ 的百分之一),因此 **frac_digits()** 通常为 2 。 以下是一个定义为 **facet** 的简单格式: **class My_money_io : public moneypunct\ {** **public:** **explicit My_money_io(siz e_t r = 0) :moneypunct\(r) { }** **char_type do_decimal_point() const { return '.'; }** **char_type do_thousands_sep() const { return ','; }** **string do_grouping() const { return "\\003\\003\\003"; }** **string_type do_curr_symbol() const { return "USD "; }** **string_type do_positive_sign() const { return ""; }** **string_type do_negative_sign() const { return "()"; }** **int do_frac_digits() const { return 2; } // two digits after decimal point** **pattern do_pos_format() const { return pat; }** **pattern do_neg_format() const { return pat; }** **private:** **static const pattern pat;** **};** **const pattern My_money_io::pat { sign, symbol, value , none };** #### **39.4.3.2** **货币输出(** Money Output**)** **money_put** 语言特征根据**moneypunct** 指定的格式写入货币金额。具体来说,**money_put** 提供 **put()** 函数,将格式合适的字符表示形式放入流的流缓冲区中: |----------------------------------------------------------------------------------------------------| | **money_put\\> facet** **(§iso.22.4.6.2)** 将值 **v** 放入缓冲区位置 **b** | | **Out put(Out b, bool intl, ios_base\& s, C fill, long double v) const;** | | **Out put(Out b, bool intl, ios_base\& s, C fill, const string_type\& v) const;** | **intl** 参数表示是使用标准的四字符"国际"货币符号还是"本地"符号(§39.4.3.1)。 已知 **money_put** ,我们可以为**Money** 定义一个输出运算符(§39.4.3): **ostream\& operator\<\<(ostream\& s, Money m)** **{** **ostream::sentry guard(s); //see** **§** **38.4.1** **if (!guard) return s;** **try {** **const money_put\\& f = use_facet\\>(s.g etloc());** **if (m==static_cast\(m)) { // m can be represented as a long long** **if (f.put(s,true ,s,s.fill(),m).failed())** **s.setstate(ios_base::badbit);** **}** **else {** **ostringstream v;** **v \<\< m; // convert to string representation** **if (f.put(s,true ,s,s.fill(),v.str()).failed())** **s.setstate(ios_base::badbit);** **}** **}** **catch (...) {** **handle_ioexception(s); // see** **§** **39.4.2.2** **}** **return s;** **}** 如果**long long** 类型的值精度不足以精确表示货币值,我会将该值转换为字符串表示形式,并使用接受字符串的 **put()** 函数输出该字符串。 #### **39.4.3.3** **货币输入(** Money Input**)** **money_get** 语言特征根据**moneypunct** 指定的格式读取货币金额。具体来说,**money_get** 提供 **get()** 函数,用于从流的流缓冲区中提取格式合适的字符表示: |--------------------------------------------------------------------------------------------------------------------------------------| | **money_g et\\> facet** **(§iso.22.4.6.1)** 将**\[b:e)** 读取到**v** 中,使用来自**s** 的格式化规则,并通过设置**r** 报告错误。 | | **In get(In b, In e, bool intl, ios_base\& s, ios_base::iostate\& r, long double\& v) const;** | | **In get(In b, In e, bool intl, ios_base\& s, ios_base::iostate\& r, string_type\& v) const;** | 良定义的**money_get** 和 **money_put** 语言特征对将提供可无错误、无信息丢失地读取的输出格式。例如: **int main()** **{** **Money m;** **while (cin\>\>m)** **cout \<\< m \<\< "\\n";** **}** 这个简单程序的输出应该可以作为其输入。此外,给定第一次运行的输出,第二次运行的输出应该与第一次运行的输出完全相同。 **Money** 类型的一个合理输入运算符是: **istream\& operator\>\>(istream\& s, Money\& m)** **{** **istream::sentry guard(s); // see _io.sentr y_** **if (guard) try {** **ios_base::iostate state = 0; // good** **istreambuf_iterator\ eos;** **string str;** **use_facet\\>(s.g etloc()).get(s,eos,true,state,str);** **if (state==0 \|\| state==ios_base::eofbit) { // set value only if get() succeeded** **long long i = stoll(str); //** **§** **36.3.5** **if (errno==ERANGE) {** **state \|= ios_base::failbit;** **}** **else {** **m = i; // set value only if conversion to long long succeeded** **}** **s.setstate(state);** **}** **}** **catch (...) {** **handle_ioexception(s); // see** **§** **39.4.2.2** **}** **return s;** **}** 我使用 **get()** 函数将数据读取到 **string** 中,因为先读取到 **double** 类型再转换为 **long** **long** 类型可能会导致精度损失。 **long double** 类型能够精确表示的最大值可能小于**long long** 类型能够表示的最大值。 ### **39.4.4** **日期和时间格式化(** Date and Time Formatting**)** 日期和时间格式由 **time_get\** 和 **time_put\** 控制。使用的日期和时间表示形式为 **tm** (§43.6)。 #### **39.4.4.1** **time_put** **time_put** 语言特征接受一个表示为 **tm** 的时间点,并使用 **strftime()** (§43.6) 或等效函数生成表示该时间点的字符序列。 |------------------------------------------------------------------------------------------------------------------| | **time_put\\> facet** **(§iso.22.4.5.1)** | | **Out put(Out s, ios_base\& f, C fill, const tm** **∗** **pt, const C** **∗** **b, const C** **∗** **e) const;** | | **Out put(Out s, ios_base\& f, C fill, const tm** **∗** **pt, char format, char mod = 0) const;** | | **Out do_put(Out s, ios_base\& ib, const tm** **∗** **pt, char format, char mod) const;** | 调用 **s=put(s,ib,fill,pt,b,e)** 将**\[b:e\]** 复制到输出流 **s** 。对于**strftime()** 中的每一个格式化字符 **x** (可选修饰符为 **mod** ),它会调用 **do_put(s,ib,pt,x,mod)** 。修饰符的可能值为 **0** (默认值,表示"无")、**E** 或 **O** 。重写函数 **p=do_put(s,ib,pt,x,mod)** 会将 **\*pt** 的相应部分格式化到 **s** 中,并返回一个指向 **s** 中最后一个写入字符之后位置的值。 标准库提供了 **messages** 的一个 **_** **byname** 版本(§39.4,§39.4.1): **template\\>** **class time_put_byname : public time_put\** **{** **// ...** **};** #### **39.4.4.2** **time_get** 基本思路是,**get_time** 可以使用相同的 **strftime()** 格式(§43.6)读取 **put_time** 生成的内容: **class time_base {** **public:** **enum dateorder {** **no_order, // meaning mdy** **dmy, //meaning "%d%m%y"** **mdy, //meaning "%m%d%y"** **ymd, // meaning "%y%m%d"** **ydm // meaning "%y%d%m"** **};** **};** **template\\>** **class time_get : public locale::facet, public time_base {** **public:** **using char_type = C;** **using iter_type = In;** **// ...** **}** 除了按格式读取,还有一些操作用于检查 **dateorder** 以及读取日期和时间表示的特定部分,例如 **weekday** 和 **monthname** : |----------------------------------------------------------------------------------------------------------------------------| | **time_get\ facet** **(§iso.22.4.5.1)** 从 **\[b:e)** 读取到 **∗** **pt** | | **dateorder do_date_order() const;** | | **In get_time(In b, In e, ios_base\& ib, ios_base::iostate\& err, tm** **∗** **pt) const;** | | **In get_date(In b, In e, ios_base\& ib, ios_base::iostate\& err, tm** **∗** **pt) const;** | | **In get_weekday(In b, In e, ios_base\& ib, ios_base::iostate\& err, tm** **∗** **pt) const;** | | **In get_monthname(In b, In e, ios_base\& ib, ios_base::iostate\& err, tm** **∗** **pt) const;** | | **In get_year(In b, In e, ios_base\& ib, ios_base::iostate\& err, tm** **∗** **pt) const;** | | **In get(In b, In e, ios_base\& ib, ios_base::iostate\& err, tm** **∗** **pt, char format, char mod) const;** | | **In get(In b, In e, ios_base\& ib, ios_base::iostate\& err, tm** **∗** **pt, char format) const;** | | **In get(In b, In e, ios_base\& ib, ios_base::iostate\& err, tm** **∗** **pt, C** **∗** **fmtb, C** **∗** **fmte) const;** | **get_** **∗** **()** 函数从 **\[b:e)** 读取内容到**∗pt** ,从 **b** 获取 **locale** ,并在出错时设置 **err** 。它返回一个指向 **\[b:e)** 中第一个未读取字符的迭代器。 调用 **p=get(b,e,ib,err,pt,format,mod)** 会读取格式字符 **format** 和修饰符字符 **mod** 指定的值,具体由 **strftime()** 函数指定。如果未指定 **mod** ,则使用**mod==0** 。 调用 **get(b,e ,ib,err,pt,fmtb,fmtb)** 时,使用以字符串形式呈现的**strftime()** 格式(例如 \[**fmtb:fmte** )。此重载以及带有默认修饰符的重载都没有 **do_get()** 接口。相反,它们是通过对第一个 **get()** 调用**do_get()** 来实现的。 时间和日期 **facet** 最显而易见的用途是为 **Date** 类提供 **locale** 相关的 I/O 。考虑一下 §16.3 中 **Date** 类的一个变体: **class Date {** **public:** **explicit Date(int d ={}, Month m ={}, int year ={});** **// ...** **string to_string(const locale\& = locale()) const;** **};** **istream\& operator\>\>(istream\& is, Date\& d);** **ostream\& operator\<\<(ostream\& os, Date d);** **Date::to_string()** 使用 **stringstream** 生成特定**locale()** 的**string** (§38.2.2): **string Date::to_string(const locale\& loc) const** **{** **ostringstream os;** **os.imbue(loc);** **return os \<\<** **∗** **this;** **}** 已经 **to_string()** 函数,输出运算符非常简单: **ostream\& operator\<\<(ostream\& os, Date d)** **{** **return os\<\\>(istream\& is, Date\& d)** **{** **if (istream::sentry guard{is}) {** **ios_base::iostate err = goodbit;** **struct tm t;** **use_facet\\>(is.g etloc()).get_date(is,0,is,err,\&t); // read into t** **if (!err) {** **Month m = static_cast\(t.tm_mon+1);** **d = Date(t.tm_day,m,t.tm_year+1900);** **}** **is.setstate(err);** **}** **return is;** **}** 需要 **+1900** ,因为 **1900** 年是 **tm** 的元年(§43.6)。 标准库提供了**messages** 的一个**_byname** 版本(§39.4, §39.4.1): **template\\>** **class time_get_byname : public time_get\ {** **// ...** **};** ### **39.4.5** **字符分类(** Character Classification**)** 从输入中读取字符时,通常需要对其进行分类才能理解读取的内容。例如,要读取一个数,输入程序需要知道哪些字母代表数字。类似地,§10.2.2 展示了如何使用标准字符分类函数来解析输入。 字符分类自然取决于所使用的字母表。因此,我们提供了一个语言特征类型 **ctype** 来表示特定 **locale** 中的字符分类。 字符类由一个名为**mask** 的枚举描述: **class ctype_base {** **public:** **enum mask { // the actual values are implementation-defined** **space = 1, // whitespace (in "C" locale:** **'** **'** **,** **'** **\\n** **'** **,** **'** **\\t** **'** **, ...)** **print = 1\<\<1, // printing characters** **cntrl = 1\<\<2, // control characters** **upper = 1\<\<3, // uppercase characters** **lower = 1\<\<4, // lowercase characters** **alpha = 1\<\<5, // alphabetic characters** **digit = 1\<\<6, // decimal digits** **punct = 1\<\<7, // punctuation characters** **xdigit = 1\<\<8, // hexadecimal digits** **blank = 1 \<\< 9; // space and horizontal tab** **alnum=alpha\|digit, // alphanumer ic characters** **graph=alnum\|punct** **};** **};** **template\** **class ctype : public locale::facet, public ctype_base {** **public:** **using char_type = C;** **// ...** **};** 此 **mask** 不依赖于特定的字符类型。因此,此枚举被放置在(非模板)基类中。 显然,**mask** 反映了传统的 C 和 C++ 分类(§36.2.1)。然而,对于不同的字符集,不同的字符值属于不同的类别。例如,在 ASCII 字符集中,整数值 **125** 代表字符"}",这是一个标点符号字符(**punct** )。但是,在丹麦语字符集中,**125** 代表元音字母**"** **å** **"** ,在丹麦语 **locale** 中,该字母必须归类为 **alpha** 。 这种分类称为"掩码",因为传统的、针对小型字符集的高效字符分类实现方法是使用一个表格,其中每一个条目包含表示分类的位。例如: **table\['P'\] == upper\|alpha** **table\['a'\] == lower\|alpha\|xdigit** **table\['1'\] == digit\|xdigit** **table\[' '\] == space\|blank** 已知该实现,如果字符**c** 是**m** ,则 **table\[c\]\&m** 为非零值,否则为**0** 。 **ctype** 语言特征的定义如下: |------------------------------------------------------------------------------------------------------------| | **ctype\ facet** **(§iso.22.4.1.1)** | | **bool is(mask m, C c) const;** | | **const C** **∗** **is(const C** **∗** **b, const C** **∗** **e, mask** **∗** **v) const;** | | **const C** **∗** **scan_is(mask m, const C** **∗** **b, const C** **∗** **e) const;** | | **const C** **∗** **scan_not(mask m, const C** **∗** **b, const C** **∗** **e) const;** | | **C toupper(C c) const;** | | **const C** **∗** **toupper(C** **∗** **b, const C** **∗** **e) const;** | | **C tolower(C c) const;** | | **const C** **∗** **tolower(C** **∗** **b, const C** **∗** **e) const;** | | **C widen(C c) const;** | | **const char** **∗** **widen(const char** **∗** **b, const char** **∗** **e, C** **∗** **b2) const;** | | **char narrow(C c, char def) const;** | | **const C** **∗** **narrow(const C** **∗** **b, const C** **∗** **e, char def, char** **∗** **b2) const;** | 调用**is(m,c)** 来测试字符 **c** 是否属于分类 **m** 。例如: **int count_spaces(const string\& s, const locale\& loc)** **{** **const ctype\\& ct = use_facet\\>(loc);** **int i = 0;** **for(auto p = s.begin(); p!=s.end(); ++p)** **if (ct.is(ctype_base::space,** **∗** **p)) //whitespace as defined by ct** **++i;** **return i;** **}** 请注意,也可以使用 **is()** 函数来检查字符是否属于多个分类之一。例如: **ct.is(ctype_base::space\|ctype_base::punct,c); // c** **是** **ct** **中的空白字符或标点吗** **?** 调用**is(b,e,v)** 确定 **\[b:e)** 中每一个字符的分类,并将其放置在数组 **v** 中的相应位置。 调用 **scan_is(m,b,e)** 返回指向集合 **\[b:e)** 中第一个为 **m** 的字符的指针。如果没有字符被判定为 **m** ,则返回 **e** 。与标准 **facet** 一样,公共成员函数是通过调用其虚函数 **do_** 来实现的。一个简单的实现可能是: **template\** **const C** **∗** **ctype\::do_scan_is(mask m, const C** **∗** **b, const C** **∗** **e) const** **{** **while (b!=e \&\& !is(m,** **∗** **b))** **++b;** **return b;** **}** 调用**scan_not(m,b,e)** 返回指向 **\[b:e)** 中第一个非**m** 字符的指针。如果所有字符都判定为 **m** ,则返回**e** 。 调用 **toupper(c)** 返回 **c** 的大写版本(若这样一个版本存在于所使用的字符集中),否则返回 **c** 本身。 调用 **toupper(b,e)** 将范围 **\[b:e)** 内的每一个字符转换为大写,并返回 **e** 。一个简单的实现方式如下: **template\** **const C** **∗** **ctype\::to_upper(C** **∗** **b, const C** **∗** **e)** **{** **for (; b!=e; ++b)** **∗** **b = toupper(** **∗** **b);** **return e;** **}** **tolower()** 函数与**toupper()** 函数类似,区别在于它会将字母转换为小写。 调用**widen(c)** 将字符**c** 转换为其对应的 C 值。如果 C 字符集中有多个与 **c** 对应的字符,则标准规定应使用"最简单的合理转换"。例如: **wcout \<\< use_facet\\>(wcout.g etloc()).widen('e');** 将输出与**wcout** 的**locale** 中的字符 **e** 相当的合理值。 使用**widen()** 函数也可以在不相关的字符表示形式(例如 **ASCII** 和 **EBCDIC** )之间进行转换。例如,假设存在一个 **ebcdic** 本地化: **char EBCDIC_e = use_facet\\>(ebcdic).widen('e');** 调用 **widen(b,e,v)** 会获取范围 **\[b:e)** 中的每一个字符,并将加宽后的版本放置在数组 **v** 中的相应位置。 调用 **narrow(ch,def)** 会生成一个与来自**C** 类型中的字符 **ch** 对应的 **char** 值。同样,这里要使用"最简单的合理转换"。如果不存在对应的 **char** ,则返回 **def** 。 调用 **narrow(b,e ,def,v)** 会获取范围 **\[b:e)** 中的每一个字符,并将缩减后的版本放置在数组 **v** 中的相应位置。 其基本思路是,**narrow()** 函数将较大的字符集转换为较小的字符集,而 **widen()** 函数执行相反的操作。对于较小字符集中的字符**c** ,我们期望: **c == narrow(widen(c),0) //** **不保证** 如果字符 **c** 表示的字符在"较小的字符集"中只有一种表示形式,那么上述说法成立。然而,这并不能保证。如果字符**char** 表示的字符不是较大字符集 **(C)** 所表示字符的子集,那么我们应该预料到,在以通用方式处理字符时,代码会出现异常和潜在问题。 类似地,对于来自较大字符集的字符 **ch** ,我们可能会期望: **widen(narrow(ch,def)) == ch \|\| widen(narrow(ch,def)) == widen(def) //** **不保证** 然而,尽管这种情况很常见,但对于在大字符集中有多个值而在小字符集中只有一个值的字符,则无法保证这一点。例如,数字 **7** 在大字符集中通常有多种不同的表示形式。这通常是因为大字符集包含多个常规字符集作为子集,而较小字符集中的字符为了便于转换会被复制。 对于基本源字符集(§6.1.2)中的每一个字符,都保证 **widen(narrow(ch_lit,0)) == ch_lit** 例如: **widen(narrow('x',0)) == 'x'** **narrow()** 和 **widen()** 函数尽可能遵循字符分类。例如,如果 **is(alpha,c)** 为真,则只要**alpha** 是所用 **locale** 的一个有效掩码,则 **is(alpha,narrow(c,'a'))** 和 **is(alpha,widen(c))** 也成立。 **使用** **ctype** **语言特征的一个主要原因(特别是使用** **narrow()** **和** **widen()** **函数)是为了能够编写对任何字符集进行 I/O 和字符串操作的代码,也就是说,使代码能够通用化,不受字符集的限制。** 这意味着 **iostream** 的实现严重依赖于这些功能。通过依赖 **\** 和 **\** ,用户可以避免大多数直接使用 **ctype** 语言特征的情况。 标准库提供了 **ctype** 的一个 **_byname** 版本: **template\** **class ctype_byname : public ctype\ {** **// ...** **};** ### **39.4.6** **字符编码转换(** Character Code Conversion**)** **有时,存储在文件中的字符表示形式与这些字符在主内存中的预期表示形式不同** 。例如,日文字符通常存储在其中的标识符("移位")标识已知字符序列属于四种常用字符集(汉字、片假名、平假名和罗马字)中的哪一种的文件中。这种方式略显繁琐,因为每一个字节的含义都取决于其"移位状态",但它可以节省内存,因为只有汉字需要超过 1 个字节的表示。在主内存中,如果使用多字节字符集表示这些字符,且每一个字符的大小相同,则更容易操作。此类字符(例如,Unicode 字符)通常放置在宽字符 (**wchar_t** ; §6.2.3) 中。**因此,** **codecvt** **语言特征提供了一种机制,用于在读取或写入字符时将其从一种表示形式转换为另一种表示形式。** 例如: ![](https://i-blog.csdnimg.cn/direct/a724aa6bb8c14e55a254d55ec3717499.png) 这种代码转换机制足够通用,可以实现任意字符表示形式的转换。它允许我们使用合适的内部字符表示形式(存储在 **char** 、**wchar_t** 或其他类型中)编写一个程序,然后通过调整 **iostream** 使用的 **locale** 来接受各种输入字符流表示形式。**另一种方法是修改程序本身,或者将输入输出文件转换为各种格式。** **codecvt** 语言特征提供在流缓冲区和外部存储之间移动字符时,不同字符集之间的转换: **class codecvt_base {** **public:** **enum result { // result indicators** **ok, partial, error, noconv** **};** **};** **template\** **class codecvt : public locale::facet, public codecvt_base {** **public:** **using intern_type = In;** **using extern_type = Ex;** **using state_type = SS;** **// ...** **};** |-----------------------------------------------------------------------------------------------------------------------------------------------| | **codecvt\ facet** **(§iso.22.5)** **using CI = const In; using CE = const Ex;** | | **result in(SS\& st, CE** **∗** **b, CE** **∗** **e, CE** **∗** **\& next, In** **∗** **b2, In** **∗** **e2, In** **∗** **\& next2) const;** | | **result out(SS\& st, CI** **∗** **b, CI** **∗** **e, CI** **∗** **\& next, Ex** **∗** **b2, Ex** **∗** **e2, Ex** **∗** **\& next2) const;** | | **result unshift(SS\& st, Ex** **∗** **b, Ex** **∗** **e, Ex** **∗** **\& next) const;** | | **int encoding() const noexcept;** | | **bool always_noconv() const noexcept;** | | **int length(SS\& st, CE** **∗** **b, CE** **∗** **e, siz e_t max) const;** | | **int max_length() const noexcept;** | 一个**codecvt** 语言特征由**basic_filebuf** (§38.2.1)用于读取或写入字符。一个**basic_filebuf** 从流的 **locale** (§38.1)中获取此 **facet** 。 **State** 模板参数是用于保存正在转换的流的移位状态的类型。**State** 还可以通过指定特化来标识不同的转换。后者非常有用,因为各种字符编码(字符集)的字符可以存储在相同类型的对象中。例如: **class JISstate { /\* .. \*/ };** **p = new codecvt\; // standard char to wide char** **q = new codecvt\; // JIS to wide char** 如果没有不同的状态参数,**facet** 就无法知道应该对字符流采用哪种编码。来自 **\** 或**\** 的 **mbstate_t** 类型标识了系统在 **char** 和 **wchar_t** 之间的标准转换。 也可以将新的 **codecvt** 创建为派生类,并用名称进行标识。例如: **class JIScvt : public codecvt\ {** **// ...** **};** 调用 **in(st,b,e,next,b2,e2,next2)** 会读取范围 **\[b:e)** 中的每一个字符并尝试将其转换。如果一个字符被转换,**in()** 会将其转换后的形式写入范围 **\[b2:e2)** 中的对应位置;否则,**in()** 会在该位置停止。返回时,**in()** 会将读取到的最后一个字符之后的位置(下一个要读取的字符)存储在**next** 中,并将写入到的最后一个字符之后的位置(下一个要写入的字符)存储在 **next2** 中。**in()** 返回的结果值标识了已完成的工作量。 |-------------|----------------------------| | **codecvt_base result** **(§iso.22.4.1.4)** || | **ok** | 所有位于 **\[b:e)** 范围内的字符均已转换 | | **partial** | 并非 **\[b:e)** 中的所有字符都被转换了。 | | **error** | 无法转换某个字符 | | **noconv** | 无需转换 | **请注意,部分转换并不一定意味着错误。可能是需要读取更多字符才能完成多字节字符的转换并写入,或者需要清空输出缓冲区以腾出空间容纳更多字符。** **state_type** 参数**st** 标识在调用 **in()** 函数开始时输入字符序列的状态。当外部字符表示使用移位状态时,这一点尤为重要。请注意,**st** 是一个(非常量)引用参数:在调用结束时,**st** 保存的是输入序列的移位状态。这使得程序员能够处理部分转换,并使用多次调用 **in()** 函数来转换一个较长的序列。 字符流必须以"中性"(未移位)状态开始和结束。通常,该状态为 **state_type{}** 。 调用 **unshift(ss,b,e,next)** 会检查字符串 **st** ,并根据需要将字符放入**\[b:e)** 中,以使字符序列恢复到未移位状态。**unshift()** 的结果以及 **next** 的使用方式与 **out()** 相同。 调用 **length(st,b,e,max)** 返回 **in()** 可以从 **\[b:e\]** 转换的字符数。**encoding()** 的返回值含义如下: **-1** ------ 外部字符集的编码使用状态(例如,使用移位和取消移位字符序列)。 **0** ------ 编码使用不同数量的字节来表示单个字符(例如,字符表示可能使用字节中的一个位来标识使用 1 个字节还是 2 个字节来表示该字符)。 **n** ------ 外部字符表示中的每一个字符都占用 **n** 个字节。 如果内部字符集和外部字符集之间不需要转换,则调用 **always_noconv()** 返回 **true** ,否则返回 **false** 。显然,**always_noconv() == true** 使得实现能够提供最高效的方案,即完全不调用转换函数。 调用 **cvt.max_length()** 返回 **cvt.length(ss,p,q,n)** 对于一组有效的参数可以返回的最大值。 我能想到的最简单的代码转换就是将输入转换为大写。因此,这已经是 **codecvt** 能提供服务的最简单的程度了: **class Cvt_to_upper : public codecvt\ { //** **转换为大写** **public:** **explicit Cvt_to_upper(size_t r = 0) : codecvt(r) { }** **protected:** **//** **读入外部表示** **,** **写入内部表示** **:** **result do_in(State\& s,** **const char** **∗** **from, const char** **∗** **from_end, const char** **∗** **\& from_next,** **char** **∗** **to, char** **∗** **to_end, char** **∗** **\& to_next** **) const override;** **// read internal representation, write exter nal representation:** **result do_out(State\& s,** **const char** **∗** **from, const char** **∗** **from_end, const char** **∗** **\& from_next,** **char** **∗** **to, char** **∗** **to_end, char** **∗** **\& to_next** **) const override;** **result do_unshift(State\&, E** **∗** **to, E** **∗** **to_end, E** **∗** **\& to_next) const override { return ok; }** **int do_encoding() const noexcept override { return 1; }** **bool do_always_noconv() const noexcept override { return false; }** **int do_length(const State\&, const E** **∗** **from, const E** **∗** **from_end, size_t max) const override;** **int do_max_length() const noexcept override; // maximum possible length()** **};** **codecvt\::result** **Cvt_to_upper::do_out(State\& s,** **const char** **∗** **from, const char** **∗** **from_end, const char** **∗** **\& from_next,** **char** **∗** **to, char** **∗** **to_end, char** **∗** **\& to_next) const** **{** **return codecvt\::do_out(s,from,from_end,from_next,to,to_end,to_next);** **}** **codecvt\::result** **Cvt_to_upper::do_in(State\& s,** **const char** **∗** **from, const char** **∗** **from_end, const char** **∗** **\& from_next,** **char** **∗** **to, char** **∗** **to_end, char** **∗** **\& to_next) const** **{** **// ...** **}** **int main() // trivial test** **{** **locale ulocale(locale(), new Cvt_to_upper);** **cin.imbue(ulocale);** **for (char ch; cin\>\>ch; )** **cout \<\< ch;** **}** 标准库提供了 **codecvt** 的一个 **_byname** 版本(§39.4,§39.4.1): **template\** **class codecvt_byname : public codecvt\ {** **// ...** **};** ### **39.4.7** **消息(** Messages**)** **大多数最终用户很显然更喜欢使用他们的母语与程序交互。** 然而,我们无法提供用于表达特定于 **locale** 的通用交互标准机制。因此,该库提供了一种简单的机制,用于维护一组特定于 **locale** 的字符串,程序员可以从中编写简单的消息。**在本质上,** **messages** **实现了一个简单的只读数据库:** **class messages_base {** **public:** **using catalog = /\* implementation-defined integer type \*/; // catalog identifier type** **};** **template\** **class messages : public locale::facet, public messages_base {** **public:** **using char_type = C;** **using string_type = basic_string\;** **// ...** **};** 消息接口相对简单: |-----------------------------------------------------------------------------------------| | **messages\ facet (** **§** **iso.22.4.7.1)** | | **catalog open(const string\& s, const locale\& loc) const;** | | **string_type get(catalog cat, int set, int id, const basic_string\\& def) const;** | | **void close(catalog cat) const;** | 调用 **open(s,loc)** 会为本地化**loc** 打开一个称为 **s** 的消息"目录"。一个目录是一组以特定于实现的方式组织的字符串,可通过 **messages::get()** 函数访问。如果无法打开名为 **s** 的目录,则返回负值。必须先打开目录,然后才能首次使用**get()** 函数。 调用**close(cat)** 关闭由**cat** 标识的目录,并释放与该目录关联的所有资源。 调用 **get(cat,set,id,"foo")** 会在目录 **cat** 中查找由 **(set,id)** 标识的消息。如果找到字符串,**get()** 返回该字符串;否则,**get()** 返回默认字符串( 此处为 **string("foo")** )。 以下是一个消息语言特征实现的示例,其中消息目录是一个"消息"集合的有序数组,"消息"是一个字符串: **struct Set {** **vector\ msgs;** **};** **struct Cat {** **vector\ sets;** **};** **class My_messages : public messages\ {** **vector\\& catalogs;** **public:** **explicit My_messages(siz e_t = 0) :catalogs{** **∗** **new vector\} { }** **catalog do_open(const string\& s, const locale\& loc) const; // open catalog s** **string do_get(catalog cat, int s, int m, const string\&) const; // get message (s,m) in cat** **void do_close(catalog cat) const** **{** **if (catalogs.size()\<=cat)** **catalogs.erase(catalogs.begin()+cat);** **}** **˜My_messages() { delete \&catalogs; }** **};** 所有 **messages** 的成员函数都是 **const** ,因此目录数据结构(**vector\** )存储在 **facet** 之外。 选择消息时,需指定一个目录、该目录中的一个集合以及该集合中的一条消息字符串。提供一个字符串作为参数,用于在目录中未找到消息时作为默认结果。 **string My_messages::do_get(catalog cat, int set, int id, const string\& def) const** **{** **if (catalogs.size()\<=cat)** **return def;** **Cat\& c = catalogs\[cat\];** **if (c.sets.size()\<=set)** **return def;** **Set\& s = c.sets\[set\];** **if (s.msgs.size()\<=msg)** **return def;** **return s.msgs\[id\];** **}** 打开目录涉及将磁盘上的文本表示读入**Cat** 结构。这里,我选择了一种易于读取的表示形式。集合由 **\<\<\<** 和 **\>\>\>** 分隔,每条消息都是一行文本: **messages\::catalog My_messages::do_open(const string\& n, const locale\& loc) const** **{** **string nn = n + locale().name();** **ifstream f(nn.c_str());** **if (!f) return** **−1;** **catalogs.push_back(Cat{}); // make in-core catalog** **Cat\& c = catalogs.back();** **for(string s; f\>\>s \&\& s=="\<\<\<"; ) { // read Set** **c.sets.push_back(Set{});** **Set\& ss = c.sets.back();** **while (getline(f,s) \&\& s != "\>\>\>") // read message** **ss.msgs.push_back(s);** **}** **return catalogs.size()** **−1;** **}** 以下是一个简单用法: **int main()** **// a trivial test** **{** **if (!has_facet\(locale())) {** **cerr \<\< "no messages facet found in" \<\< locale().name() \<\< '\\n';** **exit(1);** **}** **const messages\\& m = use_facet\(locale());** **extern string message_director y; // where I keep my messages** **auto cat = m.open(message_director y,locale());** **if (cat\<0) {** **cerr \<\< "no catalog found\\n";** **exit(1);** **}** **cout \<\< m.get(cat,0,0,"Missed again!") \<\< endl;** **cout \<\< m.get(cat,1,2,"Missed again!") \<\< endl;** **cout \<\< m.get(cat,1,3,"Missed again!") \<\< endl;** **cout \<\< m.get(cat,3,0,"Missed again!") \<\< endl;** **}** 若这个目录是: **\<\<\<** **hello** **goodbye** **\>\>\>** **\<\<\<** **yes** **no** **maybe** **\>\>\>** 程序打印: **hello** **maybe** **Missed again!** **Missed again!** #### **39.4.7.1** **使用来自其它** **facet** **的消息(** Using Messages from Other **facet** s**)** 除了作为与用户通信时使用的与 **locale** 相关的字符串的存储库,**message** 还可以用于保存其他 **facet** 所需的字符串。例如,**Season_io** 语言特征(§39.3.2)可以这样编写: **class Season_io : public locale::facet {** **const messages\\& m; // message directory** **messages_base::catalog cat; // message catalog** **public:** **class Missing_messages { };** **Season_io(size_t i = 0)** **: locale::facet(i),** **m(use_facet\(locale())),** **cat(m.open(message_director y,locale()))** **{** **if (cat\<0)** **throw Missing_messages();** **}** **˜Season_io() { } // to make it possible to destroy Season_io objects (** **§** **39.3)** **const string\& to_str(Season x) const; // string representation of x** **bool from_str(const string\& s, Season\& x) const; // place Season corresponding to s in x** **static locale::id id; // facet identifier object (** **§** **39.2,** **§** **39.3,** **§** **39.3.1)** **};** **locale::id Season_io::id; // define the identifier object** **string Season_io::to_str(Season x) const** **{** **return m** **−\>get(cat,0,x,"no−such−season");** **}** **bool Season_io::from_str(const string\& s, Season\& x) const** **{** **for (int i = Season::spring; i\<=Season::winter; i++)** **if (m** **−\>get(cat,0,i,"no−such−season") == s) {** **x = Season(i);** **return true;** **}** **return false;** **}** 这种基于 **messages** 的解决方案与原始解决方案(§39.3.2)的不同之处在于,为新 **locale** 的 **Season** 字符串集的实现者需要能够将其添加到消息目录中。对于向执行环境添加新 **locale** 的人员来说,这很容易。但是,由于 **messages** 仅提供只读接口,因此添加新的季节名称集可能超出了应用程序员的能力范围。 标准库提供了 **messages** 的一个 **_byname** 版本(§39.4,§39.4.1): **template\** **class messages_byname : public messages\ {** **// ...** **};** ## **39.5** **便捷接口(** Convenience Interfaces**)** 除了简单地注入 I/O **iostream** 之外,**locale** 功能可能使用起来很复杂。因此,我们提供了**便捷的接口**来简化表示法并最大限度地减少错误。 ### **39.5.1** **字符分类(** Character Classification**)** **ctype** 语言特征最常见的用途是查询字符是否属于给定的分类。因此,我们提供了一组函数来实现这一目的: |---------------------|----------------------------------------------------------| | **locale** 敏感字符分类(§iso.22.3.3.1) || | **isspace(c,loc)** | **c** 是 **loc** 中的空格吗? | | **isblank(c,loc)** | **c** 是**loc** 中的空格吗? | | **isprint(c,loc)** | **c** 可打印吗? | | **iscntrl(c,loc)** | **c** 是一个控制字符吗? | | **isupper(c,loc)** | **c** 是一个大写字母吗? | | **islower(c,loc)** | **c** 是一个小写字母吗? | | **isalpha(c,loc)** | **c** 是一个字母吗? | | **isdigit(c,loc)** | **c** 是一个十进制数吗? | | **ispunct(c,loc)** | **c** 不是一个字母、数字、空白、或不可见控制字符吗? | | **isxdigit(c,loc)** | **c** 是一个十六进制数字吗? | | **isalnum(c,loc)** | **isalpha(c)** 或**isdigit(c)** | | **isgraph(c,loc)** | **isalpha(c)** 或**isdigit(c)** 或**ispunct(c)** (注意:不是空格) | 这些功能使用 **use_facet** 可以轻松实现。例如: **template\** **inline bool isspace(C c, const locale\& loc)** **{** **return use_facet\\>(loc).is(space,c);** **}** 这些函数的单参数版本(§36.2.1)使用当前的 C 全局区域本地化。除了极少数 C 全局本地化与 C++ 全局本地化不同的情况(§39.2.1),我们可以将单参数版本视为应用于 **locale()** 函数的双参数版本。例如: **inline int isspace(int i)** **{** **return isspace(i,locale()); // almost** **}** ### **39.5.2** **字符转换(** Character Conversions**)** 大小写转换可能与 **locale** 有关: |------------------------|---------------------------------------------| | 字符转换**(§iso.22.3.3.2.1)** || | **c2= toupper(c,loc)** | **use_facet\\>(loc).toupper(c)** | | **c2= tolower(c,loc)** | **use_facet\\>(loc).tolower(c)** | ### **39.5.3** **字符串转换(** String Conversions**)** 字符编码转换可能与 **locale** 有关。**类模板** **wstring_convert** **执行宽字符串和字节字符串之间的转换。** 它允许你指定一个编码转换 **facet** (例如 **codecvt** ) 来执行转换,而不会影响任何流或 **locale** 。例如,你可以直接使用名为 **codecvt_utf8** 的编码转换 **facet** ,将 UTF-8 多字节序列输出到 **cout** ,而不会更改 **cout** 的**locale** : **wstring_conver t\\> myconv;** **string s = myconv.to_bytes(L"Hello\\n");** **cout \<\< s;** **wstring_convert** 的定义相当传统: **template\, // wide-character allocator** **class Ba = std::allocator\ // byte allocator** **\>** **class wstring_conver t {** **public:** **using byte_string = basic_string\, Ba\>;** **using wide_string = basic_string\, Wa\>;** **using state_type = typename Codecvt::state_type;** **using int_type = typename wide_string::traits_type::int_type;** **// ...** **};** **wstring_convert** 构造函数允许我们指定一个字符转换 **facet** ,初始转换状态以及发生错误时要使用的值: |----------------------------------------|--------------------------------------------------------------------------------------| | **wstring_conver t\** **(§iso.22.3.3.2.2)** || | **wstring_convert cvt {};** | **wstring_conver t cvt {new Codecvt};** | | **wstring_convert cvt {pcvt,state}** | **cvt** 使用了转换语言特征**∗** **pcvt** 和转换状态 **state** | | **wstring_convert cvt {pcvt};** | **wstring_conver t cvt {pcvt,state_type{}};** | | **wstring_convert cvt {b_err,w_err};** | **wstring_conver t cvt{};** 使用**b_error** 和**w_err** | | **wstring_convert cvt {b_err};** | **wstring_conver t cvt{};** 使用**b_error** | | **cvt.** **˜wstring_conver t();** | 析构函数 | | **ws=cvt.from_bytes(c)** | **ws** 包含转换为 **Wc** 的**char c** 。 | | **ws=cvt.from_bytes(s)** | **ws** 包含转换为 **Wc** 格式的 **s** 的 **char** ; **s** 是一个 C 风格字符串或一个**string** 。 | | **ws=cvt.from_bytes(b,e)** | **ws** 包含转换为 **Wc** 的**\[b:e)** 的**char** **。** | | **s=cvt.to_bytes(wc)** | **s** 包含转换为**char** 的 **wc** | | **s=cvt.to_bytes(ws)** | **s** 包含转换为**char** 的 **ws** 的 **Wc** ; **ws** 是一个 C 风格的字符串或一个**basic_string\** | | **s=cvt.to_bytes(b,e)** | **s** 包含转换为 **char** 的 **\[b:e)** 的 **Wc** | | **n=cvt.conver ted()** | **n** 是由 **cvt** 转换的输入元素的数量 | | **st=cvt.state()** | **st** 是 **cvt** 的状态 | 如果转换为**wide_string** 失败,则使用非默认 **w_err** 字符串构造的**cvt** 上的函数将返回该字符串(作为错误消息);否则,它们将抛出**range_error** 。 如果转换为**byte_string** 失败,则使用非默认 **b_err** 字符串构造的 **cvt** 上的函数将返回该字符串(作为错误消息);否则,它们将抛出 **range_error** 。 例如: **void test()** **{** **wstring_conver t\\> conver ter;** **string s8 = u8"This is a UTF** **−8 string";** **wstring s16 = conver ter.from_bytes(s8);** **string s88 = conver ter.to_bytes(s16);** **if (s8!=s88)** **cerr \<"Insane!\\n";** **}** ### **39.5.4** **缓冲区转换(** Buffer Conversions**)** 我们可以使用代码转换 **facet** (§39.4.6)直接写入流缓冲区或直接从流缓冲区读取数据(§38.6): **template\** **\>** **class wbuffer_convert** **: public std::basic_streambuf\ {** **public:** **using state_type = typename Codecvt::state_type;** **// ...** **};** |------------------------------------------|----------------------------------------------------------------------------------| | **wbuffer_conver t\** **(§iso.22.3.3.2.3)** || | **wbuffer_convert wb {psb,pcvt,state};** | **wb** 使用转换器 **∗** **pcvt** 和初始转换状态**state** ,从 **streambuf** **∗** **psb** 进行转换 | | **wbuffer_convert wb {psb,pcvt};** | **wbuffer_conver t wb {psb,pcvt,state_type{}};** | | **wbuffer_convert wb {psb}; wbuffer_** | **wbuffer_conver t wb {psb,new Codecvt{}};** | | **wbuffer_convert wb {};** | **wbuffer_conver t wb {nullptr};** | | **psb=wb.rdbuf()** | **psb** 是 **wb** 的流缓冲区 | | **psb2=wb.rdbuf(psb)** | 将 **wb** 的流缓冲区设置为 **\*psb** **;** **∗** **psb2** 是 **wb** 的前一个流缓冲区 | | **t=wb.state()** | **t** 的 **wb** 转换状态 | ## **39.6** **建议(** Advice**)** \[1\] 预期所有与用户直接交互的非平凡程序或系统都将在多个不同国家/地区使用;§39.1。 \[2\] 不要假设每个人都使用与你相同的字符集;§39.1,§39.4.1。 \[3\] 优先使用 **locale** 来编写文化相关的 I/O 专用代码;§39.1。 \[4\] 使用 **locale** 来满足外部(非 C++)标准;§39.1。 **\[5\]** **将** **locale** **视为包含多个** **facet** **的容器;§39.2。** \[6\] 避免在程序文本中嵌入 **locale** 名称字符串;§39.2.1。 **\[7\]** **尽量只在程序中的少数几个地方更改** **locale** ;§39.2.1。 \[8\] 尽量减少全局格式信息的使用;§39.2.1。 \[9\] 优先使用 **locale** 敏感的字符串比较和排序;参见 §39.2.2、§39.4.1。 \[10\] 使 **facet** 不可变;参见 §39.3。 \[11\] 让 **locale** 控制 **facet** 的生命周期;参见 §39.3。 \[12\] 你可以创建自己的 **facet** ;参见 §39.3.2。 \[13\] 编写 **locale** 敏感的 I/O 函数时,请记住处理用户提供的(重写)函数的异常;参见 §39.4.2.2。 \[14\] 如果需要在数中使用分隔符,请使用 **numput** ;参见 §39.4.2.1。 \[15\] 使用简单的 **Money** 类型来存储货币值;参见 §39.4.3。 \[16\] 使用简单的用户定义类型来存储要求 **locale** 敏感的 I/O 的值(而不是在内置类型的值之间进行强制转换)。 §39.4.3 。 \[17\] **time_put** 语言特征可用于**\** 和 **\** 两种类型的时间。§39.4.4 。 \[18\] 优先使用显式指定 **locale** 的字符分类函数;§39.4.5,§39.5 。 **内容来源:** **\<\\> 第4版,作者 Bjarne Stroustrup**

相关推荐
繁华似锦respect10 小时前
C++ 智能指针底层实现深度解析
linux·开发语言·c++·设计模式·代理模式
Bona Sun11 小时前
单片机手搓掌上游戏机(二十三)—esp32运行简单街机模拟器软硬件准备
c语言·c++·单片机
@小码农11 小时前
2025年北京海淀区中小学生信息学竞赛第二赛段C++真题
开发语言·数据结构·c++·算法
sulikey12 小时前
C++模板初阶详解:从函数模板到类模板的全面解析
开发语言·c++·模板·函数模板·类模板
0 0 012 小时前
CCF-CSP第39次认证第三题——HTTP 头信息(HPACK)【C++】
开发语言·c++·算法
汉克老师12 小时前
2023年海淀区中小学信息学竞赛复赛(小学组试题第二题 回文时间 (time))
c++·算法·北京海淀中小学信息竞赛·模拟法
还没想好取啥名13 小时前
C++11新特性(一)——原始字面量
开发语言·c++
天赐学c语言13 小时前
12.6 - K个一组翻转链表 && C 编译到执行的4个阶段
数据结构·c++·链表·c编译
爪哇部落算法小助手14 小时前
每日两题day65
数据结构·c++·算法