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

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

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

目录

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

39.2  类locale (Class locale)

39.2.1  命名locale (Named locales)

39.2.1.1  构造新的locale (Constructing New locales)

39.2.2  比较字符串 (Comparing strings)

39.3  类facet (Class facet)

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

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

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

39.4  标准 facet (Standard facets)

39.4.1  string 比较 (string  Comparison)

39.4.1.1  命名collate(Named collate)

39.4.2  数值格式化(Numeric Formatting)

39.4.2.1  数值标点(Numeric Punctuation)

39.4.2.2  数值输出(Numeric Output)

39.4.2.3  数值输入(Numeric Input)

39.4.3  货币格式化(Money Formatting)

39.4.3.1  货币标点(Money Punctuation)

39.4.3.2  货币输出(Money Output)

39.4.3.3  货币输入(Money Input)

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

39.4.4.1  time_put

39.4.4.2  time_get

39.4.5  字符分类(Character Classification)

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

39.4.7  消息(Messages)

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

39.5  便捷接口(Convenience Interfaces)

39.5.1  字符分类(Character Classification)

39.5.2  字符转换(Character Conversions)

39.5.3  字符串转换(String Conversions)

39.5.4  缓冲区转换(Buffer Conversions)

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>

locale成员(§iso.22.3.1)

locale loc {};

loc 是当前全局语言环境的副本;noexcept

locale loc {loc2};

复制构造函数:loc 存储 loc2 的副本;loc.name()==loc2.name();noexcept

locale loc {s};

loc 初始化为具有名称 slocales 可以是string或 C 风格字符串;loc.name()==sexplicit

locale loc {loc2,s,cat};

locloc2 的副本,除了类别为 cat 的方面,它是从 locale{s} 复制而来的;s 可以是string或 C 风格字符串;如果 loc2 有名称,则 loc 也有名称

locale loc {loc2,pf};

loc loc2 的一个副本,除了对于 facet pf

前提是 pf!=nullptrloc 没有名称

locale loc {loc2,loc3,cat};

loc loc2 的副本,除了 cat 类别的facet,它是从 loc3 复制而来的;如果 loc2 loc3 都有名称,那么 loc 也有一个名称

loc.˜locale()

析构函数;非虚函数;noexcept

loc2=loc

赋值:loc2 loc 的副本;noexcept

loc3=loc.combine<F>(loc2)

loc3 loc 的一个副本,除了 facet F

facet F 是从 loc2 复制而来的;loc3 没有名称

s=loc.name()

sloc 的语言环境名称或

loc==loc2

loc loc2 是同一个˜locale吗?

loc!=loc2

!(loc==loc2)

loc()(s,s2)

使用 loccollat​​e<C> 方面比较 basic_string<C>ss2

loc2=global(loc)

将全局语言环境设置为 locloc2 是前一个全局locale

loc=classic()

loc 是经典的“C”语言环境

如果已知名称的locale或所引用的facet不存在,则命名它的locale操作将引发runtime_error

locale的命名有点奇怪。当你基于另一个locale加上一个facet创建新的locale时,并且生成的locale有一个名称时,该名称是由实现定义的。通常,这种由实现定义的名称包含提供大部分facetlocale的名称。对于没有名称的localename() 返回

可以将 locale 视为 map<id,facet> 的接口,也就是说,它允许我们使用 locale::id 来查找 locale::facet 派生类的对应对象。locale 的实际实现是这种思想的有效变体。布局如下所示:

这里,collat​​e<char> numpunct<char> 是标准库 facet(§39.4)。所有 facet 均派生自 locale::facet

    locale旨在自由且低成本地复制。因​​此,locale几乎肯定会实现为构成其主要实现部分的特定 map<id,facet> 的句柄。在locale中,各个方面必须能够快速访问。因此,特定 map<id,facet> 将被优化以提供类似数组的快速访问。localefacet可以通过 use_facet<Facet>(loc) 表示法访问;参见 §39.3.1。

    标准库提供了丰富的facet。为了帮助程序员按逻辑分组操作facet,标准facet分为不同的类别,例如numericcollate (§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 中。每个 istreamostream 都有其自己的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 locales)

    一个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 locales)

    创建新的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可以表示如下:

如果 Facet* 参数(此处为 My_money_io)为 nullptr,则生成的locale只是locale参数的副本。

    在构造函数 locale{loc,f } 中,参数 f 必须标识一个特定的 facet 类型。普通的 facet 是不够的。例如:

void g(const locale::facet mio1, const money_put<char> 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<char>)

// ...

}

locale使用 Facet* 参数的类型在编译时确定 facet 的类型。具体来说,locale的实现使用 facet 的标识类型 facet::id(§39.3)在locale中查找该 facet(§39.3.1)。构造函数

template<class Facet> locale(const locale& x, Facet f);

是该语言中唯一允许程序员通过locale提供facet的机制。其他locale由实现者以命名本地化设置的形式提供(§39.2.1)。命名本地化可以从程序的执行环境中获取。了解特定实现机制的程序员或许能够添加新的locale

    locale构造函数集的设计使得每一个 facet 的类型都可以通过类型推导(根据 Facet 模板参数)或来自其他locale (已知其类型)来得知。指定 category 参数间接指定了 facet 的类型,因为locale已知 categoryfacet 的类型。这意味着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 strings)

根据locale比较两个string可能是locale除了 I/O 操作之外最常见的用途。因此,locale直接提供了此操作,用户无需根据collatefacet(§39.4.1)构建自己的比较函数。此string比较函数定义为localeoperator()()。例如:

void user(const string s1, const string s2, const locale& my_locale)

{

if (my_locale(s,s2)) { // is s<s2 according to my_locale?

// ...

}

}

将比较函数作为 () 运算符,使其可以直接用作谓词(§4.5.4)。例如:

void f(vector<string>& 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 位于 <locale> 中:

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<id,facet*>

    用于定义(派生)facet 的数据在派生类中定义。这意味着定义 facet 的程序员对数据拥有完全控制权,并且可以使用任意数量的数据来实现 facet 所表示的概念。

    facet 的设计初衷是不可变的,因此用户定义的 facet 的所有成员函数都应该定义为 const

39.3.1  locale中访问facets (Accessing facets in a  locale)

可以通过两个模板函数访问locale的各个facet

非成员 locale 函数

f=use_facet<F>(loc)

f 是指向 loc facet F 的引用;如果 loc 中没有 facet F,则抛出 bad_cast 异常。

has_facet<F>(loc)

loc 是否有facet F noexcept

可以将这些函数视为在其locale参数中查找模板参数 F 。或者,可以将 use_facet 视为将locale显式转换为特定 facet 的一种方式。这是可行的,因为一个facet只能有一个已经类型的 facet 。例如:

void f(const locale& my_locale)

{

char c = use_facet<numpunct<char>>(my_locale).decimal_point() // use standard facet

// ...

if (has_facet<Encrypt>(my_locale)) { // does my_locale contain an Encrypt facet?

const Encrypt& f = use_facet<Encrypt>(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<char>collat​​e_byname<char> 在同一locale中可以互换,因此它们都使用 collat​​e<char>::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仅限于charstring

    Season_io 类为所有 Season_io 文化特征提供了一个通用的抽象接口。要定义特定locale 环境下的 Season的 I/O 表示,我们需要从 Season_io 类派生出一个类,并相应地定义 to_str() from_str() 函数。

    输出Season信息很简单。如果数据流包含 Season_io 的文化特征,我们可以使用它把值转换成字符串。否则,我们可以直接输出Seasonint值:

ostream& operator<<(ostream& os, Season x)

{

locale loc {os.getloc()}; // extract the streams locale (§38.4.4)

if (has_facet<Season_io>(loc))

return os << use_facet<Season_io>(loc).to_str(x); // string representation

return os << static_cast<int>(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 streams locale (§38.4.4)

if (has_facet<Season_io>(loc)) {

const Season_io& f {use_facet<Season_io>(loc)}; // get hold of the locales 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<Season>(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<spring || winter<x) {

static const string ss = "no−such−season";

return ss;

}

return seasons[x];

}

bool US_season_io::from_str(const string& s, Season& x) const

{

const string p = find(begin(seasons),end(seasons),s);

if (p==end)

return false;

x = Season(p−begin(seasons));

return true;

}

请注意,由于 US_season_io 只是 Season_io 接口的一个实现,因此我没有为 US_season_io 定义 id 。实际上,如果我们希望 US_season_io 用作 Season_io,则不能为其分配 id 。诸如 has_facet(§39.3.1)之类的本地化设置操作依赖于实现相同概念的 facet 由相同的 id 标识(§39.3)。

    唯一值得关注的实现问题是,如果被要求输出一个无效的Season该怎么办。当然,这种情况不应该发生。然而,对于简单的用户自定义类型,遇到无效值的情况并不少见,因此考虑这种可能性是合理的。我本可以抛出异常,但对于供人阅读的简单输出,通常为超出范围的值生成一个“超出范围”的表示形式会很有帮助。请注意,对于输入,错误处理策略由 >> 运算符负责,而对于输出,facet 函数 to_str() 实现了错误处理策略。这样做是为了说明不同的设计方案。在“生产环境设计”中,facet 函数要么同时实现输入和输出的错误处理,要么只报告错误信息,以便 >> << 运算符进行处理。

    此 Season_io 设计依赖于派生类来提供特定本地化的字符串。另一种设计方案是让 Season_io 本身从特定于本地化的存储库中检索这些字符串(参见 §39.4.7)。至于是否可以只使用一个 Season_io 类,并将季节字符串作为构造函数参数传递给它,则留待后续研究。

39.3.3  使用localefacet (Uses of  locales  and facets)

    标准库中本地化的主要用途是用于 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 facets)

<locale> 中,标准库提供了以下facet

标准facet (§iso.22.3.1.1.1)

collate

字符串比较

collate<C>

§39.4.1

numeric

数值格式化

numpunct<C>

num_get<C,In>

num_put<C,Out>

§39.4.2

monetary

货币格式化

moneypunct<C>

moneypunct<C,International>

money_get<C,In>

money_put<C,Out>

§39.4.3

time

日期和时间格式化

time_put<C,Out>

time_put_byname<C,Out>

time_get<C,In>

§39.4.4

ctype

字符分类

ctype<C>

codecvt<In,Ex,SS>

codecvt_byname<In,Ex,SS>

§39.4.5

messages

消息检索

messages<C>

§39.4.7

详情请参见相关小节。

    从该表中实例化 facet 时,C 必须是字符类型(§36.1)。这些 facet 保证为 char wchar_t 类型定义。此外,ctype<C> 保证支持 char16_t char32_t 类型。如果需要使用标准 I/O 处理其他字符类型 X,则必须依赖特定于实现的 facet 特化,或为 X 提供合适的 facet 版本。例如,可能需要 codecvt<X,char,mbstate_t>(§39.4.6)来控制 Xchar 之间的转换。

    International可以是truefalsetrue表示使用货币符号的三字符(加上零终止符)“international”表示(§39.4.3.1),例如 USDBRL

移位状态参数 SS 用于表示多字节字符表示的移位状态(§39.4.6)。在 <cwchar> 中,mbstate_t 定义为,表示可以出现在一个由实现所定义的支持多字节字符编码规则集中的任何转换状态。对于任意字符类型 X mbstate_t 的等效项是char_traits<X>::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 FF_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<string>& v, const locale& loc)

{

const collate<char>& col {use_facet<collate<char>>(dk)};

const ctype<char>& ctyp {use_facet<ctype<char>>(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 的新版本。换句话说,除了简单的操作(例如对 iostreamimbue() 和对 sort() collat​​e),locale机制并不适合新手直接使用。有关本地化设置的详细讨论,请参阅 [Langer,2000]。

    单个语言特征的设计通常比较复杂。部分原因是这些语言特征必须反映库设计者无法控制的复杂文化习俗,部分原因是 C++ 标准库的功能必须与 C 标准库和各种平台特定标准的功能保持高度兼容。

    在另一方面,localefacet提供的框架具有通用性和灵活性。facet可以设计成承载任何数据,并且facet的操作可以根据这些数据提供任何所需的操作。如果新facet的行为没有受到过多的约定限制,那么它的设计可以做到简洁明了(§39.3.2)。

39.4.1  string 比较 (string  Comparison)

    标准collate语言特征提供了比较字符数组的方法:

template<class C>

class collate : public locale::facet {

public:

using char_type = C;

using string_type = basic_string<C>;

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<C> 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<char>& 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比较,我们可以使用collatecompare() 函数。例如:

void f(const string& s1, const string& s2, const string& name)

{

bool b {s1==s2}; // compare using implementations 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<char>;

const Col& global {use_facet<Col>(locale{})}; // from the current global locale

int i0 {global.compare(s1b,s1e,s2b,s2e)};

const Col& my_coll {use_facet<Col>(locale{""})}; // from my preferred locale

int i1 {my_coll.compare(s1b,s1e ,s2b,s2e)};

const Col& n_coll {use_facet<Col>(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进行比较

// ...

}

不难想象 i0i1i2 不同的情况。请看以下这段来自德语词典的单词序列:

Dialekt, Di ¨ at, dich, dichten, Dichtung

按照惯例,名词(仅名词)首字母大写,但字母顺序不区分大小写。

区分大小写的德语排序会将所有以 D 开头的单词排在 d 之前:

Dialekt, Di ¨ at, Dichtung, dich, dichten

ä ( 元音变音 a ) 被视为“一种 a”,因此它排在 c 之前。然而,在大多数常用字符集中,ä 的数值大于 c 的数值。因此,int('c')<int('a') ,基于数值的简单默认排序结果为:

Dialekt, Dichtung, Di¨ at, dich, dichten

编写一个能够根据字典正确排序此序列的比较函数是一个有趣的练习。

39.4.1.1  命名collate(Named collate)

collat​​e_byname 是针对由构造函数字符串参数命名的localecollat​​e 的一个版本:

template<class C>

class collate_byname : public collate<C> { // note: no id and no new functions

public:

typedef basic_string<C> 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  数值格式化(Numeric Formatting)

数值输出由 num_put 语言特征写入流缓冲区完成(§38.6)。相反,数值输入由 num_get 语言特征从流缓冲区读取完成。num_put num_get 使用的格式由名为 numpunct 的“数值标点” facet 定义。

39.4.2.1  数值标点(Numeric Punctuation)

numpunct 语言特征定义了内置类型(例如 boolintdouble)的 I/O 格式:

numpunct<C> 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<char> {

public:

explicit My_punct(size_t r = 0) :numpunct<char>(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。如果 iostreamboolalpha 标志已设置(参见 §38.4.5.1),则 truename() falsename() 返回的字符串分别用于表示truefalse;否则,分别使用 1 0

    标准库提供了 numpunct 的一个 _byname 版本(§39.4,§39.4.1):

template<class C>

class numpunct_byname : public numpunct<C> {

// ...

};

39.4.2.2  数值输出(Numeric Output)

当写入流缓冲区时(§38.6),ostream 依赖于 num_put 语言特征:

num_put<C,Out=ostreambuf_iterator<C>> 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<C> )是标准locale的一部分(§39.4)。要在其他地方使用 num_put 写入数据,我们必须定义一个合适的特化。例如,以下是一个非常简单的用于写入字符串的 num_put 示例:

template<class C>

class String_numput : public num_put<C,typename basic_string<C>::iterator> {

public:

String_numput() :num_put<C,typename basic_string<C>::iterator>{1} { }

};

我并不打算让 String_numput 进入locale,所以我使用了构造函数参数来保留其常规的生命周期规则。预期用法大致如下:

void f(int i, string& s, int pos) // format i into s starting at pos

{

String_numput<char> f;

f.put(s.begin()+pos,cout,' ',i); // format i into s; use couts for matting rules

}

ios_base 参数(此处为 cout)提供有关格式化状态和locale的信息。例如:

void test(iostream& io)

{

locale loc = io.getloc();

wchar_t wc = use_facet<ctype<char>>(loc).widen(c); // char to C conversion

string s = use_facet<numpunct<char>>(loc).decimal_point(); // default: .

string false_name = use_facet<numpunct<char>>(loc).falsename(); // default: "false"

}

num_put<char> 这样的标准 facet 通常通过标准的 I/O 流函数隐式使用。因此,大多数程序员无需了解它。然而,标准库函数对这些 facet 的使用方式很有意思,因为它们展示了 I/O 流的工作原理以及如何使用 facet 。一如既往,标准库提供了许多有趣的编程技巧示例。

使用 num_putostream 的实现者可能会这样写:

template<class C, class Tr>

basic_ostream<C,Tr>& basic_ostream<C,Tr>::operator<<(double d)

{

sentry guard(this); // see §38.4.1

if (!guard) return this;

try {

if (use_facet<num_put<C,Tr>>(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<class C, class Tr>

void handle_ioexception(basic_ostream<C,Tr>& 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<In = istreambuf_iterator<C>> 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<class C, class Tr>

basic_istream<C,Tr>& basic_istream<C,Tr>::operator>>(double& d)

{

sentry guard(this); // see §38.4.1

if (!guard) return this;

iostate state = 0; // good

istreambuf_iterator<C,Tr> eos;

try {

double dd;

use_facet<num_g et<C,Tr>>(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)。输出将根据 coutlocale而变化。可能的输出包括:

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)

控制货币金额呈现的facetmoneypunct 自然类似于控制普通数的facetnumpunct(§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 C, bool International = false>

class moneypunct : public locale::facet, public money_base {

public:

using char_type = C;

using string_type = basic_string<C>;

// ...

};

moneypunct 成员函数定义了货币输入和输出的布局:

moneypunct<C,International>> 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 C, bool Intl = false>

class moneypunct_byname : public moneypunct<C, Intl> {

// ...

};

decimal_point()thousands_sep() grouping() 成员的行为与 numpunct 中的行为相同。

    curr_symbol()positive_sign() negative_sign() 成员分别返回用于表示货币符号(例如 $¥INRDKK)、加号和减号的字符串。如果 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"

signvalue 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<char,true> {

public:

explicit My_money_io(siz e_t r = 0) :moneypunct<char,true>(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<C,Out = ostreambuf_iterator<C>> 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<char>& f = use_facet<money_put<char>>(s.g etloc());

if (m==static_cast<long long>(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<C,In = istreambuf_iterator<C>> 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<char> eos;

string str;

use_facet<money_g et<char>>(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<C,In> time_put<C,Out> 控制。使用的日期和时间表示形式为 tm(§43.6)。

39.4.4.1  time_put

    time_put 语言特征接受一个表示为 tm 的时间点,并使用 strftime() (§43.6) 或等效函数生成表示该时间点的字符序列。

time_put<C,Out = ostreambuf_iterator<C>> 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 C, class Out = ostreambuf_iterator<C>>

class time_put_byname : public time_put<C,Out>

{

// ...

};

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 C, class In = istreambuf_iterator<C>>

class time_get : public locale::facet, public time_base {

public:

using char_type = C;

using iter_type = In;

// ...

}

除了按格式读取,还有一些操作用于检查 dateorder 以及读取日期和时间表示的特定部分,例如 weekday monthname

time_get<C,In> 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<<to_string(d,os.getloc());

}

输入操作符需要注意状态:

istream& operator>>(istream& is, Date& d)

{

if (istream::sentry guard{is}) {

ios_base::iostate err = goodbit;

struct tm t;

use_facet<time_get<char>>(is.g etloc()).get_date(is,0,is,err,&t); // read into t

if (!err) {

Month m = static_cast<Month>(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 C, class In = istreambuf_iterator<C>>

class time_get_byname : public time_get<C, In> {

// ...

};

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 C>

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<C> 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<char>& ct = use_facet<ctype<char>>(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,则返回 。与标准 facet 一样,公共成员函数是通过调用其虚函数 do_ 来实现的。一个简单的实现可能是:

template<class C>

const C ctype<C>::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<class C>

const C ctype<C>::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<ctype<wchar_t>>(wcout.g etloc()).widen('e');

将输出与 wcout locale 中的字符 e 相当的合理值。

使用 widen() 函数也可以在不相关的字符表示形式(例如 ASCII EBCDIC)之间进行转换。例如,假设存在一个 ebcdic 本地化:

char EBCDIC_e = use_facet<ctype<char>>(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 的实现严重依赖于这些功能。通过依赖 <iostream> <string>,用户可以避免大多数直接使用 ctype 语言特征的情况。

    标准库提供了 ctype 的一个 _byname 版本:

template<class C>

class ctype_byname : public ctype<C> {

// ...

};

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

    有时,存储在文件中的字符表示形式与这些字符在主内存中的预期表示形式不同。例如,日文字符通常存储在其中的标识符(“移位”)标识已知字符序列属于四种常用字符集(汉字、片假名、平假名和罗马字)中的哪一种的文件中。这种方式略显繁琐,因为每一个字节的含义都取决于其“移位状态”,但它可以节省内存,因为只有汉字需要超过 1 个字节的表示。在主内存中,如果使用多字节字符集表示这些字符,且每一个字符的大小相同,则更容易操作。此类字符(例如,Unicode 字符)通常放置在宽字符 (wchar_t; §6.2.3) 中。因此,codecvt 语言特征提供了一种机制,用于在读取或写入字符时将其从一种表示形式转换为另一种表示形式。例如:

这种代码转换机制足够通用,可以实现任意字符表示形式的转换。它允许我们使用合适的内部字符表示形式(存储在 charwchar_t 或其他类型中)编写一个程序,然后通过调整 iostream 使用的 locale 来接受各种输入字符流表示形式。另一种方法是修改程序本身,或者将输入输出文件转换为各种格式。

    codecvt 语言特征提供在流缓冲区和外部存储之间移动字符时,不同字符集之间的转换:

class codecvt_base {

public:

enum result { // result indicators

ok, partial, error, noconv

};

};

template<class In, class Ex, class SS>

class codecvt : public locale::facet, public codecvt_base {

public:

using intern_type = In;

using extern_type = Ex;

using state_type = SS;

// ...

};

codecvt<In,Ex,SS> 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<wchar_t,char,mbstate_t>; // standard char to wide char

q = new codecvt<wchar_t,char,JISstate>; // JIS to wide char

如果没有不同的状态参数,facet 就无法知道应该对字符流采用哪种编码。来自 <cwchar> <wchar.h> mbstate_t 类型标识了系统在 char wchar_t 之间的标准转换。

    也可以将新的 codecvt 创建为派生类,并用名称进行标识。例如:

class JIScvt : public codecvt<wchar_t,char,mbstate_t> {

// ...

};

调用 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<char,char,mbstate_t> { // 转换为大写

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<char,char,mbstate_t>::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<char,char,mbstate_t>::do_out(s,from,from_end,from_next,to,to_end,to_next);

}

codecvt<char,char,mbstate_t>::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 I, class E, class State>

class codecvt_byname : public codecvt<I,E,State> {

// ...

};

39.4.7  消息(Messages)

       大多数最终用户很显然更喜欢使用他们的母语与程序交互。然而,我们无法提供用于表达特定于 locale 的通用交互标准机制。因此,该库提供了一种简单的机制,用于维护一组特定于 locale 的字符串,程序员可以从中编写简单的消息。在本质上,messages 实现了一个简单的只读数据库:

class messages_base {

public:

using catalog = /* implementation-defined integer type */; // catalog identifier type

};

template<class C>

class messages : public locale::facet, public messages_base {

public:

using char_type = C;

using string_type = basic_string<C>;

// ...

};

消息接口相对简单:

messages<C> 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<C>& 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<string> msgs;

};

struct Cat {

vector<Set> sets;

};

class My_messages : public messages<char> {

vector<Cat>& catalogs;

public:

explicit My_messages(siz e_t = 0) :catalogs{new vector<Cat>} { }

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<Set>)存储在 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<char>::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<My_messages>(locale())) {

cerr << "no messages facet found in" << locale().name() << '\n';

exit(1);

}

const messages<char>& m = use_facet<My_messages>(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 facets)

    除了作为与用户通信时使用的与 locale 相关的字符串的存储库,message 还可以用于保存其他 facet 所需的字符串。例如,Season_io 语言特征(§39.3.2)可以这样编写:

class Season_io : public locale::facet {

const messages<char>& 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<Season_messages>(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 C>

class messages_byname : public messages<C> {

// ...

};

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<class C>

inline bool isspace(C c, const locale& loc)

{

return use_facet<ctype<C>>(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<ctype<C>>(loc).toupper(c)

c2= tolower(c,loc)

use_facet<ctype<C>>(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<codecvt_utf8<wchar_t>> myconv;

string s = myconv.to_bytes(L"Hello\n");

cout << s;

wstring_convert 的定义相当传统:

template<class Codecvt,

class Wc = wchar_t,

class Wa = std::allocator<Wc>, // wide-character allocator

class Ba = std::allocator<char> // byte allocator

>

class wstring_conver t {

public:

using byte_string = basic_string<char, char_traits<char>, Ba>;

using wide_string = basic_string<Wc, char_traits<Wc>, Wa>;

using state_type = typename Codecvt::state_type;

using int_type = typename wide_string::traits_type::int_type;

// ...

};

wstring_convert 构造函数允许我们指定一个字符转换 facet ,初始转换状态以及发生错误时要使用的值:

wstring_conver t<Codecvt,Wc,Wa,Ba> (§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<Wc>

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<codecvt_utf8_utf16<wchar_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 Codecvt,

class C = wchar_t,

class Tr = std::char_traits<C>

>

class wbuffer_convert

: public std::basic_streambuf<C,Tr> {

public:

using state_type = typename Codecvt::state_type;

// ...

};

wbuffer_conver t<Codecvt,C,Tr> (§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 语言特征可用于 <chrono> <ctime> 两种类型的时间。§39.4.4 。

[18] 优先使用显式指定 locale 的字符分类函数;§39.4.5,§39.5 。

内容来源:

<<The C++ Programming Language >> 第4版,作者 Bjarne Stroustrup

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值