使用 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 等图书馆。
我有一个字符迭代器——一个包裹在几个适配器中的 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 等图书馆。