使用 STL 的迭代器上的 UTF-8 到 UTF-32

UTF-8 to UTF-32 on iterators using the STL

我有一个字符迭代器——一个包裹在几个适配器中的 std::istreambuf_iterator<char>——生成 UTF-8 字节。我想从中读取单个 UTF-32 字符(char32_t)。我可以使用 STL 这样做吗?怎么样?

std::codecvt_utf8<char32_t>,但这显然只适用于 char*,不适用于任意迭代器。

这是我的代码的简化版本:

#include <iostream>
#include <sstream>
#include <iterator>

// in the real code some boost adaptors etc. are involved
// but the important point is: we're dealing with a char iterator.
typedef std::istreambuf_iterator< char > iterator;

char32_t read_code_point( iterator& it, const iterator& end )
{
    // how do I do this conversion?
    // codecvt_utf8<char32_t>::in() only works on char*
    return U'[=10=]';
}

int main()
{
    // actual code uses std::istream so it works on strings, files etc.
    // but that's irrelevant for the question
    std::stringstream stream( u8"\u00FF" );
    iterator it( stream );
    iterator end;
    char32_t c = read_code_point( it, end );
    std::cout << std::boolalpha << ( c == U'\u00FF' ) << std::endl;
    return 0;
}

我知道 Boost.Regex 有一个迭代器,但我想避免使用不是 header-only 的增强库,这感觉像是 STL 应该能够做到的。

我认为您不能直接使用 codecvt_utf8 或任何其他标准库组件来执行此操作。要使用 codecvt_utf8,您需要将迭代器流中的字节复制到缓冲区并转换缓冲区。

像这样的东西应该可以工作:

char32_t read_code_point( iterator& it, const iterator& end )
{
  char32_t result;
  char32_t* resend = &result + 1;
  char32_t* resnext = &result;
  char buf[7];  // room for 3-byte UTF-8 BOM and a 4-byte UTF-8 character
  char* bufpos = buf;
  const char* const bufend = std::end(buf);
  std::codecvt_utf8<char32_t> cvt;
  while (bufpos != bufend && it != end)
  {
    *bufpos++ = *it++;
    std::mbstate_t st{};
    const char* be = bufpos;
    const char* bn = buf;
    auto conv = cvt.in(st, buf, be, bn, &result, resend, resnext);
    if (conv == std::codecvt_base::error)
      throw std::runtime_error("Invalid UTF-8 sequence");
    if (conv == std::codecvt_base::ok && bn == be)
      return result;
    // otherwise read another byte and try again
  }
  if (it == end)
    throw std::runtime_error("Incomplete UTF-8 sequence");
  throw std::runtime_error("No character read from first seven bytes");
}

这似乎做了比必要更多的工作,在每次迭代时重新扫描 [buf, bufpos) 中的整个 UTF-8 序列(并对 codecvt_utf8::do_in 进行虚函数调用)。理论上,codecvt_utf8::in 实现可以读取一个不完整的多字节序列并将状态信息存储在 mbstate_t 参数中,以便下一次调用将从上次停止的地方恢复,只消耗新字节,而不是重新调用-处理已经看到的不完整的多字节序列。

但是,不需要实现使用 mbstate_t 参数来存储调用之间的状态,实际上 codecvt_utf8::in 的至少一个实现(我为 GCC 写的一个)根本不使用它。从我的实验来看,libc++ 实现似乎也不使用它。这意味着它们在不完整的多字节序列之前停止转换,并让 from_next 指针(此处的 bn 参数)指向该不完整序列的开头,以便下一次调用应从该位置开始并且(希望)提供足够的额外字节来完成序列并允许读取完整的 Unicode 字符并将其转换为 char32_t。因为您只是试图读取单个代码点,这意味着它根本不进行任何转换,因为在不完整的多字节序列之前停止意味着在第一个字节处停止。

某些实现 do 可能使用 mbstate_t 参数,因此您可以修改上面的函数来处理这种情况,但为了便于移植,它会仍然需要应对忽略 mbstate_t 的实现。支持这两种类型的实现会使函数变得相当复杂,所以我保持简单并编写了一个适用于所有实现的表单,即使它们确实使用了 mbstate_t。因为您一次最多只能读取 7 个字节(在最坏的情况下......平均情况可能只有一个或两个字节,具体取决于输入文本)重新扫描前几个字节的成本每次都不应该很大。

要从 codecvt_utf8 获得更好的性能,您应该避免一次转换一个代码点,因为它是为转换字符数组而不是单个字符而设计的。由于无论如何您总是需要复制到 char 缓冲区,因此您可以从输入迭代器序列中复制更大的块并转换整个块。这将减少看到不完整的多字节序列的可能性,因为如果块以不完整的序列结尾,则只有块末尾的最后 1-3 个字节需要重新处理,块中较早的所有内容都将被转换.

为了获得更好的读取单个代码点的性能,您可能应该完全避免 codecvt_utf8 并自己滚动(如果您只需要 UTF-8 到 UTF-32BE 并不难)或使用第三方ICU 等图书馆。