【问题标题】:How do you cope with signed char -> int issues with standard library?您如何处理标准库的签名 char -> int 问题?
【发布时间】:2011-12-26 08:28:51
【问题描述】:

这是我工作中长期存在的问题,我意识到我仍然没有很好的解决方案...

C 天真地为 int 定义了所有的字符测试函数:

int isspace(int ch);

但是 char 通常是有符号的,并且一个完整的字符通常不适合 int 或任何用于字符串******的单个存储单元。

这些函数已经成为当前 C++ 函数和方法的逻辑模板,并为当前的标准库奠定了基础。事实上,它们仍然受到支持,afaict。

因此,如果您使用 isspace(*pchar),您最终可能会遇到符号扩展问题。它们很难看到,因此根据我的经验,它们很难防范。

同样,因为 isspace() 和它的同类都采用整数,并且因为一个字符的实际宽度通常是未知的,没有字符串分析 - 这意味着任何现代字符库本质上都不应该在 char 或 wchar_t 周围移动,但是只有指针/迭代器,因为只有通过分析字符流才能知道它有多少组成了一个逻辑字符,我不知道如何最好地解决这些问题?

我一直期待一个真正强大的库,它基于抽象出任何字符的大小因子,并且只使用字符串(提供诸如 isspace 等),但要么我错过了它,要么还有另一个更简单的库你们所有人(谁知道自己在做什么)都在使用的解决方案让我眼前一亮……


** 这些问题不会出现在可以完全包含完整字符的固定大小的字符编码中 - UTF-32 显然是具有这些特征的唯一选项(或将自身限制为 ASCII 或一些这样的)。


所以,我的问题是:

“你如何测试空白、可打印等,以一种不会遇到两个问题的方式:

1) 符号扩展,以及
2) 可变宽度字符问题

毕竟,大多数字符编码都是可变宽度的:UTF-7、UTF-8、UTF-16,以及诸如 Shift-JIS 之类的旧标准。如果编译器将 char 视为有符号的 8 位单元,即使扩展的 ASCII 也可能存在简单的符号扩展问题。

请注意:

无论你的 char_type 是什么大小,对于大多数字符编码方案来说都是错误的。

这个问题存在于标准C库中,也存在于C++标准库中;它仍然试图传递 char 和 wchar_t,而不是各种 isspace、isprint 等实现中的字符串迭代器。

实际上,正是那些类型的函数破坏了 std::string 的通用性。如果它只在存储单元中起作用,并且不试图假装将存储单元的含义理解为逻辑字符(例如 isspace),那么抽象会更诚实,并且会迫使我们程序员看在其他地方寻找有效的解决方案...

谢谢

所有参与的人。在这次讨论和WChars, Encodings, Standards and Portability 之间,我对这些问题有了更好的处理。虽然没有简单的答案,但每一点理解都会有所帮助。

【问题讨论】:

  • 1.你为什么在乎? 2. ctype.h 中的函数不适用于宽字符,它们在wctype.h 中。至于可变宽度 Unicode 字符,AFAIK 标准 C 库不支持它们。您可能需要使用诸如 ICU 之类的库来确定此类字符的特征。此外,字符并不总是 8 位宽。有几个流行的 16 位字符平台。您可以通过检查limits.h 中的CHAR_BIT 预处理器符号来确定字符大小。
  • 至于为什么关心?因为它实际上是在国际软件中咬我的。我现在正在调试一个问题,归结为为我们的日本经销商签署多宽度字符扩展。每个人都应该关心,因为这是我亲自使用过的每个字符串库的根本失败——大多数开发人员甚至没有意识到这些库是不够的,并且由于围绕这个问题的思考不足,他们的代码充满了问题.
  • 您一直在绝对地谈论标准未指定的事情。 wchar_t 并不总是 16 位,它是由实现定义的。它的签名也是如此。这也适用于chars。如果您认真对待软件的国际化,您应该使用支持 Unicode 的库来处理字符串,而不是标准的 C 库。后者无法处理代理对之类的事情,例如,任何类型的 Unicode 编码。
  • 我是认真的 - 所以 - 你说的这个“unicode 感知库”是什么? (此外,C/C++ 根本没有定义任何这些,这完全是一种逃避,有效地将这种混乱推倒在我们程序员身上——几乎为我们提供了有效的工具,但并不完全——至少对于任何 Unicode 编码都没有我知道——这肯定是我们在 98% 的计算领域都同意的事实标准,不是吗?)
  • char 可能未签名。 int 总是如此。而char 可能不是 8 位宽。

标签: c++ c character-encoding special-characters


【解决方案1】:

如何测试空格、isprintable 等,并且不会遇到两个问题:
1) 符号扩展
2) 可变宽度字符问题
毕竟,所有常用的 Unicode 编码都是可变宽度的,无论程序员是否意识到:UTF-7、UTF-8、UTF-16,以及 Shift-JIS 等较旧的标准......

显然,您必须使用支持 Unicode 的库,因为您已经(正确地)证明了 C++03 标准库不是。 C++11 库得到了改进,但对于大多数用途来说仍然不够好。是的,某些操作系统具有 32 位 wchar_t,这使它们能够正确处理 UTF32,但这是一种实现,并且不受 C++ 保证,并且对于许多 unicode 任务(例如遍历 Graphemes(字母))来说远远不够.

IBMICU
Libiconv
microUTF-8
UTF-8 CPP, version 1.0
utfproc
还有更多http://unicode.org/resources/libraries.html

如果问题不是关于特定字符测试,而是关于一般代码实践:做你的框架做的任何事情。如果您正在为 linux/QT/networking 编写代码,请将所有内容都保存在 UTF-8 内部。如果您使用 Windows 进行编码,请将所有内容都保存在 UTF-16 内部。如果您需要弄乱代码点,请将所有内容都保存在 UTF-32 内部。否则(对于可移植的通用代码),随心所欲,因为无论如何,您必须为某些操作系统或其他操作系统进行翻译。

【讨论】:

  • 错了。标准 C++确实 通过文字和标准库支持 Unicode。此外,char 的定义方式也能适应这种情况。
  • @ybungalobill:如果您遵守以下限制,UTF-8 编码方案仅限于以 6 个八位字节编码的 31 位:(1)0xFE 和 0xFF 无效(2)序列长度可以从第一个八位字节确定
  • @MooingDuck:“当权者决定他们永远不需要...”中的狂妄自大很强烈。肯定是错的。就像 7 位已经绰绰有余......直到它不是。对于任何固定大小的情况也是如此:只有变大才能使常见情况的低效率变得令人震惊。 ;)
  • 实际上,我刚刚计算出所有曾经使用过的语言和随机符号(包括克林贡语),Unicode 联盟只分配了大约 9.78% 的代码点。由于那是从 2011 年编写样本开始的,我们可以推断当前的 unicode 编码应该让我们再坚持大约 18543 年。
  • @Mordachai:(a) 您认为在 Unicode 中实际上值得支持的字母表中字符数量的翻倍时间大约是多少年? (b) 您预计 IPv6 地址何时用完,您的等效问题在哪里询问如何编写 TCP 堆栈来处理网络地址不能合理地固定宽度的事实? ;-)
【解决方案2】:

您的序言论点有些不准确,并且可以说是不公平的,在库设计中根本不支持 Unicode 编码 - 当然不是多个 Unicode 编码。

C 和 C++ 语言以及许多库的开发早于 Unicode 的开发。同样作为系统级语言,它们需要一种与执行环境的最小可寻址字长相对应的数据类型。不幸的是,char 类型可能已经被重载以表示执行环境的字符集和最小可寻址字。历史表明这可能是有缺陷的,但是改变语言定义和库确实会破坏大量遗留代码,所以这些事情留给了新的语言,例如具有 8 位 byte 的 C#和不同的char 类型。

此外,Unicode 表示的可变编码使其不适合这样的内置数据类型。您显然知道这一点,因为您建议应该对字符串而不是机器字类型执行 Unicode 字符操作。这将需要库支持,正如您指出的那样,标准库不提供。这有很多原因,但主要是它不在标准库的范围内,就像没有标准库支持网络或图形一样。该库本质上不解决从深度嵌入式到超级计算机的所有目标平台通常不普遍支持的任何内容。所有这些东西都必须由系统或第三方库提供。

对多字符编码的支持是关于系统/环境互操作性的,而库也不打算支持这一点。不兼容的编码系统之间的数据交换是应用程序问题而不是系统问题。

“你如何测试空白、可打印等,以一种方式 没有两个问题:

1) 符号扩展,以及

2) 可变宽度字符问题

isspace() 只考虑低 8 位。它的定义明确指出,如果您传递的参数不能表示为无符号字符或等于宏 EOF 的值,则结果是未定义的。如果按预期使用,则不会出现问题。问题是它不适合您应用它的目的。

毕竟,所有常用的 Unicode 编码都是可变宽度的, 不管程序员是否意识到:UTF-7、UTF-8、UTF-16,以及 作为旧标准,例如 Shift-JIS

isspace() 没有为 Unicode 定义。您将需要一个旨在使用您正在使用的任何特定编码的库。这个问题What is the best Unicode library for C? 可能是相关的。

【讨论】:

  • -1 用于证明对 UTF-8 的无知。 OP 实际上确实正确区分了 char 为 8 位和“字符”为可变宽度。 C(++) char 不是字符!
  • @dan04:我并没有声称对 UTF8 有任何了解,我故意避开这个主题,因为我知道我的处境很不稳定;在我开发的嵌入式系统中没有太多需要它。然而你是对的,但到目前为止他甚至没有提到 Unicode,并且似乎使用了可互换的术语。在上下文中,我认为这是模棱两可的。关于char 不是一个字符(而是一个小整数)的品脱应该是写给 Mordachai 的;似乎是他试图以这种方式使用它 - 或者至少抱怨它不起作用的事实。
  • @dan04:我删除了明显有问题的段落。整个段落完全不清楚的事实是评论而不是答案。
  • 进一步缓和,以免出现在 Mordachai 有点争论的诱饵中,并且更具建设性。
【解决方案3】:

符号扩展问题很容易处理。您可以使用:

  • isspace((unsigned char) ch)
  • isspace(ch & 0xFF)
  • 使char 成为无符号类型的编译器选项

至于可变长度字符问题(我假设为 UTF-8),这取决于您的需求。

如果您只是处理 ASCII 空白字符\t\n\v\f\r,那么isspace 可以正常工作;非 ASCII UTF-8 代码单元将被简单地视为非空格。

但如果您需要识别额外的 Unicode 空格字符\x85\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000,那就有点麻烦了。您可以按照以下方式编写一个函数

bool isspace_utf8(const char* pChar)
{
    uint32_t codePoint = decode_char(*pChar);
    return is_unicode_space(codePoint);
}

decode_char 将 UTF-8 序列转换为相应的 Unicode 代码点,is_unicode_space 对于类别为 Z 的字符或为空格的 Cc 字符返回 true。 iswspace 可能对后者有帮助,也可能没有帮助,这取决于您的 C++ 库对 Unicode 的支持程度。最好使用专用的 Unicode 库来完成这项工作。

实际上大多数字符串都使用多字节编码,例如 UTF-7, UTF-8、UTF-16、SHIFT-JIS 等

没有程序员会使用 UTF-7 或 Shift-JIS 作为内部表示,除非他们喜欢痛苦。坚持使用 ŬTF-8、-16 或 -32,并且只根据需要进行转换。

【讨论】:

  • 我很欣赏许多深思熟虑的回应。它帮助我扩展了我对这些问题的思考。我确实想让您知道,许多程序是使用当前语言环境的多字节代码页编写的——据我所知,这包括 Shift-JIS(或非常接近它的东西)。我们的主要软件实际上是为 MBCS 编译的,因此使用可变字符长度是我们的标准。就像我们切换到 UTF-16(原生 Windows)一样,因为这也是可变宽度编码。这就是为什么很难证明从我们当前的窄字符转换为宽字符的痛苦......
【解决方案4】:

前面有一条评论:像 isspace 这样的旧 C 函数将 int 用于 一个原因:他们也支持EOF 作为输入,所以他们需要能够 支持比char 多一个值。这 “天真”的决定是允许签署char——但是 使其未签名将对 a 产生严重的性能影响 PDP-11。

现在回答你的问题:

1) 符号扩展

C++ 函数没有这个问题。在 C++ 中, “正确”的测试方式,比如一个角色是否是 空格是从您想要的任何语言环境中获取std::ctype 方面, 并使用它。当然,<locale> 中的 C++ 本地化具有 经过精心设计,使其尽可能难以使用,但如果 你正在做任何重要的文本处理,你很快就会想出 您自己的便利包装器:采用语言环境的功能对象 和掩码指定您要测试的特征并不难。 使其成为面具上的模板,并为其 locale 参数提供 默认为全局语言环境也不是火箭科学。扔一个 很少的 typedef,您可以将 IsSpace() 之类的东西传递给 std::find。 唯一的微妙之处是管理std::ctype 对象的生命周期 你正在处理。然而,类似以下的东西应该可以工作:

template<std::ctype_base::mask mask>
class Is  //  Must find a better name.
{
    std::locale myLocale;
            //< Needed to ensure no premature destruction of facet
    std::ctype<char> const* myCType;
public:
    Is( std::locale const& l = std::locale() )
        : myLocale( l )
        , myCType( std::use_facet<std::ctype<char> >( l ) )
    {
    }
    bool operator()( char ch ) const
    {
        return myCType->is( mask, ch );
    }
};

typedef Is<std::ctype_base::space> IsSpace;
//  ...

(鉴于 STL 的影响,令人惊讶的是 标准没有将上述内容定义为标准。)

2) 可变宽度字符问题。

没有真正的答案。这一切都取决于你需要什么。对于一些 应用程序,只是寻找一些特定的单字节字符是 足够,并将所有内容保存在 UTF-8 中,并忽略多字节 问题,是一个可行(且简单)的解决方案。除此之外,往往 用于转换为 UTF-32(或取决于您使用的文本类型) 处理,UTF-16),并将每个元素用作单个代码点。为了 全文处理,另一方面,你必须处理 即使您使用的是 UTF-32,也可以使用多码点字符:序列 \u006D\u0302 是单个字符(一个小的 m 带有一个抑扬符 它)。

【讨论】:

    【解决方案5】:

    您似乎将 7 位 ascii 上定义的函数与通用空间识别函数混淆了。标准 C 中的字符函数使用 int 不是为了处理不同的编码,而是允许 EOF 成为带外指示符。符号扩展没有问题,因为定义这些函数的数字没有第 8 位。提供具有这种可能性的字节是您的错误。

    计划 9 尝试使用 UTF 库解决此问题,并假设所有输入数据都是 UTF-8。这允许在一定程度上与 ASCII 向后兼容,因此不兼容的程序不会全部死掉,但允许正确编写新程序。

    C 中的常见概念,甚至仍然是char* 代表一个字母数组。相反,它应该被视为一个输入数据块。要从此流中获取信件,请使用chartorune()。每个Rune 都是一个字母(/symbol/codepoint)的表示,因此可以最终定义一个函数isspacerune(),它最终会告诉您哪些字母是空格。

    像使用 char 数组一样使用 Rune 数组来进行字符串操作,然后在写出之前调用 runetochar() 将字母重新编码为 UTF-8。

    【讨论】:

    • 鉴于组合标记的存在,Rune 表示一个字母,它必须能够保存一系列代码点。
    【解决方案6】:

    在任何情况下,将 EOF 以外的负值传递给 isspace 和其他字符宏都是无效的。如果你有一个char c,并且你想测试它是否是一个空格,请执行isspace((unsigned char)c)。这处理扩展(通过零扩展)。 isspace(*pchar) 是完全错误的——不要写它,当你看到它时不要让它站立。如果你在看到它时训练自己恐慌,那么它就不那么难看到了。

    fgetc(例如)已经返回 EOF 或读取为 unsigned char 的字符,然后转换为 int,因此不存在符号扩展问题。

    不过,这确实是琐事,因为标准字符宏不涵盖 Unicode 或多字节编码。如果您想正确处理 Unicode,那么您需要一个 Unicode 库。我没有研究过 C++11 或 C1X 在这方面提供了什么,除了 C++11 有 std::u32string 这听起来很有希望。在此之前,答案是使用特定于实现的东西或第三方。 (不)幸运的是有很多库可供选择。

    可能(我推测)一个“完整”的 Unicode 分类数据库是如此之大并且如此容易发生变化,以至于 C++ 标准无论如何都要求“完全”支持是不切实际的。这在一定程度上取决于应该支持哪些操作,但你无法摆脱 Unicode 在 20 年内经历了 6 个主要版本(从第一个标准版本开始),而 C++ 在 13 年内经历了 2 个主要版本的问题.就 C++ 而言,Unicode 字符集是一个快速移动的目标,因此它总是由实现定义系统知道的代码点。

    一般来说,处理 Unicode 文本的正确方法有以下三种:

    1. 在所有 I/O(包括返回或接受字符串的系统调用)中,在外部使用的字符编码和内部固定宽度编码之间进行转换。您可以将其视为输入的“反序列化”和输出的“序列化”。如果您有一些具有将其与字节流转换为/从字节流转换的函数的对象类型,那么您不会将字节流与对象混为一谈,或者检查字节流的各个部分以查找您认为自己识别的序列化数据的 sn-ps。对于这个内部 unicode 字符串类,它不必有任何不同。请注意,类不能std::string,也可能不是std::wstring,具体取决于实现。假装标准库不提供字符串,如果有帮助的话,或者使用std::basic_string 像容器一样大的东西,但是一个支持 Unicode 的库来做任何复杂的事情。您可能还需要了解 Unicode 规范化、处理组合标记等,因为即使在固定宽度的 Unicode 编码中,每个字形也可能有多个代码点。

    2. 混淆字节序列和 Unicode 序列的一些临时混合,仔细跟踪哪个是哪个。它类似于 (1),但通常更难,因此虽然它可能是正确的,但在实践中它可能很容易出错。

    3. (仅限特殊用途):对所有内容都使用 UTF-8。有时这已经足够好了,例如,如果您所做的只是根据 ASCII 标点符号解析输入,并连接字符串以进行输出。基本上,它适用于您不需要了解任何设置最高位的程序,只需将其不加改动地传递即可。如果您需要实际渲染文本,或者对它做一些人类认为“显而易见”但实际上很复杂的事情,它就不能很好地工作。喜欢排序规则。

    【讨论】:

    • 我认为大多数 linux 程序都使用 UTF-8,因为大多数 linux 库都使用 UTF-8,而且大多数程序不需要做太多的事情。
    • @MooingDuck:是的,因为大多数程序只对代码点字符串感兴趣,而不对任何高度复杂的东西感兴趣。例如“词”。如果有人想知道如何正确使用isspace,并且还想知道 Unicode,那么他们就进入了 UTF-8 无法轻松使用的领域。 Linux 具有 wchar_t 可以表示 Unicode 代码点的后备方案,这至少是 UTF-8 无法做到的开始。
    【解决方案7】:

    我认为你混淆了一大堆不相关的概念。

    首先,char 只是一种数据类型。它的首要含义是“系统的基本存储单元”,即“一个字节”。它的签名有意留给实现,以便每个实现都可以选择最合适的(即硬件支持的)版本。它的名字,暗示着“字符”,很可能是 C 编程语言设计中最糟糕的决定。

    下一个概念是文本字符串。在基础上,文本是一系列单元,通常被称为“字符”,但它可以比这更复杂。为此,Unicode 标准创造了术语“代码点”来指定最基本的文本单元。目前,对于我们程序员来说,“文本”是一个代码点序列。

    问题在于代码点多于可能的字节值。这个问题可以通过两种不同的方式来解决:1)使用多字节编码将码位序列表示为字节序列;或 2) 使用不同的基本数据类型。 C 和 C++ 实际上提供 both 解决方案:本机主机接口(命令行参数、文件内容、环境变量)以 byte 序列的形式提供;但是该语言还为“系统的字符集”提供了不透明类型wchar_t,以及它们之间的翻译功能(mbstowcs/wcstombs)。

    不幸的是,“系统的字符集”和“系统的多字节编码”没有什么特别的,所以你和你之前的许多 SO 用户一样,对如何处理这些神秘的宽字符感到困惑。现在人们想要的是一种可以跨平台共享的明确编码。我们为此目的拥有的唯一有用的编码是 Unicode,它为大量代码点(目前最多 221)分配文本含义.除了文本编码之外,还有一系列字节串编码,UTF-8、UTF-16 和 UTF-32。

    因此,检查给定文本字符串的内容 的第一步是将其从您拥有的任何输入转换为明确(Unicode)编码的字符串。这个 Unicode 字符串本身可以以任何转换格式编码,但最简单的只是作为原始代码点序列(通常是 UTF-32,因为我们没有有用的 21 位数据类型)。

    执行这种转换已经超出了 C++ 标准(甚至是新标准)的范围,因此我们需要一个库来执行此操作。由于我们对“系统的字符集”一无所知,因此我们还需要库来处理它。

    一个受欢迎的图书馆选择是iconv();典型的序列从输入多字节char* 通过mbstowcs()std::wstringwchar_t* 宽字符串,然后通过iconv() 的WCHAR_T 到UTF32 转换为std::u32stringuint32_t* 原始Unicode 代码点序列。

    至此,我们的旅程结束。我们现在可以通过代码点检查文本代码点(这可能足以判断某物是否是空格);或者我们可以调用一个更重的文本处理库来对我们的 Unicode 代码点流执行复杂的文本操作(例如规范化、规范化、表示转换等)。这远远超出了通用程序员的范围,也超出了文本处理专家的范围。

    【讨论】:

    • “文本处理专家的领域” - 是的。有点令人沮丧的是,CS101 标准“反转字符串”超出了典型专业程序员的知识范围......
    • @SteveJessop:我认为这证明了人类写作的丰富性,以及人类思想的丰富性。以数字方式捕捉它非常!但我们只做了十多年,所以我认为我们做得还不错。古腾堡会感到自豪!
    • 我对这个答案的问题(就它而言是正确的)是它不能有效地适用于 Windows 的实际编程。操作系统希望所有内容都使用 UTF-16,但我们需要的 I/O 有其他需求,而且标准 C++ 库并不能真正正确处理 UTF-16(例如 isspace)。所以我想知道:我现在可以实现的实用方法是什么(考虑到我有一个庞大的混合 7 位、8 位和 16 位代码的代码库,可以与以各种方式编写的 API 对话,这尤其困难) C 的各个阶段以及后来的 C++ 标准。
    • @Mordachai:在内部将所有内容保留为wchar_t*std::wstring 怎么样?然后你可以直接在Win32中使用(因为Windows实际上是把宽字符串固定为UTF-16编码的),你仍然可以使用std::isspace(str, std::locale(""));
    • +1 表示“C 编程语言设计中最糟糕的决定”。
    【解决方案8】:

    我没有过多地测试 Qt 库的国际化功能,但据我所知,QString 完全支持 unicode,并且使用的是 unicode-chars 的 QChar。我不知道这些的内部实现,但我希望这意味着 QChar 是可变大小的字符。

    将自己绑定到 Qt 这样大的框架只是为了使用字符串会很奇怪。

    【讨论】:

    • 是的,它会的,特别是因为我们已经有了使用 C 库、C++ std:: 库、MFC CStrings 和 Win32 API 的代码! Yeesh - 我需要一个真正正确且健壮的字符串。 :)
    • QString 可以使用本地化编解码器轻松地在 std::string 和 std::wstring 之间进行转换。反过来,它们很容易转换为与 win32 API 配合良好的 c 字符串。我唯一不太了解的是 MFC 字符串,但我确信转换是可能的。无论如何,为什么有这么多不同的格式?您是否在一个项目中使用不同的库/代码片段?
    猜你喜欢
    • 2020-08-21
    • 2011-06-11
    • 1970-01-01
    • 2012-01-28
    • 1970-01-01
    • 1970-01-01
    • 2016-01-15
    • 2011-04-29
    • 2015-03-01
    相关资源
    最近更新 更多