C++20新增了char8_t
和std::u8string
用于UTF-8编码。但是,没有UTF-8版本的std::cout
和操作系统API大多数只接受char
和执行字符集,所以我们仍然需要一种方法在UTF-8编码和执行字符集之间进行转换。
我重新阅读了一篇char8_t论文,看起来唯一的在UTF-8编码和执行字符集之间转换的方法是使用std::c8rtomb
和std::mbrtoc8
函数。然而,它们的API非常令人困惑。有人可以提供一个示例代码吗?
std::u8fstream file; std::u8string line; ... file << line;
std::u8string text = std::format(u8"...{}...", 42);
std::string
和std::u8string
之间没有简单的转换(或者说转换)。甚至const char*
和const char8_t*
之间也没有。因此,如果你想要格式化(使用std::format
)或者输入/输出(std::cin
,std::cout
,std::fstream
,...)UTF-8数据,你必须在内部复制所有字符串。- 这将是一个不必要的性能杀手。printf("%s", (char const *)u8"ひらがな");
-- 这可以用于打印。目前为止。 - Chef Gladiatorstd::c8rtomb
和 std::mbrtoc8
,它们允许在执行编码和 UTF-8 之间进行转换。这些接口比较笨拙,它们是为了匹配预先存在的接口(如 std::c16rtomb
和 std::mbrtoc16
)而设计的。对于这些新接口增加到 C++ 标准中的措辞故意与现有相关函数的 C 标准措辞相匹配(希望这些新函数最终也能被添加到 C 中;我仍然需要追求这一点)。匹配 C 标准措辞的目的是确保任何熟悉 C 措辞的人都会认识到 char8_t
接口的工作方式相同。char8_t
变体很有用。
C++权威人士在每年的CppCon会议上(例如2018和2019年)通常给出的答案是应该选择自己喜欢的UTF8库。有各种各样的风格,只需选择您喜欢的即可。目前C++方面对Unicode的支持和理解还非常有限。
有些人希望C++23中有相关的改进,但目前我们甚至没有一个官方的工作组。
std::u8string
,因为它在标准中得到了很差的支持,并且任何系统API都不支持它(由于兼容性原因,这很可能永远不会得到支持)。在大多数平台上,普通的char
字符串已经是UTF-8格式,在Windows平台上使用MSVC编译时可以添加/utf-8
选项,在主要操作系统上获得可移植的Unicode支持。如果你还在为utf8和C++而苦恼,那么好消息是,在等待多年后,我们终于迎来了utf8的春天(2022年11月)。请参见下面Tom的评论。
我认为在短短几个月内,“3”都将实现utf8。有点运气的话,C++23将最终包含完整的utf8实现。正如早在C++11时所宣传的那样。
刚刚检查了这台机器上的cl.exe(版本号19.34.31933),没有标准的,但已经完全实现了。
// with warnings but prints ok on LINUX
// g++ prog.cc -Wall -Wextra -std=c++2a
//
// clang++ prog.cc -Wall -Wextra -std=c++2a
// lot of warnings but prints OK on LINUX
//
#include <cassert>
#include <clocale>
#include <cstdio>
#include <cstdlib> // MB_CUR_MAX
#include <cuchar>
#undef P_
#undef P
#define P_(F_, X_) printf("\n%4d : %32s => " F_, __LINE__, #X_, (X_))
#define P(F_, X_) P_(F_, X_)
/*
using mbstate_t = ... see description ...
using size_t = ... see description ...
in the standard but not implemented yet by any of the three
size_t mbrtoc8(char8_t* pc8, const char* s, size_t n, mbstate_t* ps);
size_t c8rtomb(char* s, char8_t c8, mbstate_t* ps);
*/
namespace {
constexpr inline auto bad_size = ((size_t)-1);
// https://en.wikipedia.org/wiki/UTF-8
// a compile time constant not intrinsic function
constexpr inline int UTF8_CHAR_MAX_BYTES = 4;
#ifdef STANDARD_CUCHAR_IMPLEMENTED
template <size_t N>
auto char_star(const char8_t (&in)[N]) noexcept {
mbstate_t state;
constexpr static int out_size = (UTF8_CHAR_MAX_BYTES * N) + 1;
struct {
char data[out_size];
} out = {{0}};
char* one_char = out.data;
for (size_t rc, n = 0; n < N; ++n) {
rc = c8rtomb(one_char, in[n], &state);
if (rc == bad_size) break;
one_char += rc;
}
return out;
}
#endif // STANDARD_CUCHAR_IMPLEMENTED
template <size_t N>
auto char_star(const char16_t (&in)[N]) noexcept {
mbstate_t state;
constexpr static int out_size = (UTF8_CHAR_MAX_BYTES * N) + 1;
struct final {
char data[out_size];
} out = {{0}};
char* one_char = out.data;
for (size_t rc, n = 0; n < N; ++n) {
rc = c16rtomb(one_char, in[n], &state);
if (rc == bad_size) break;
one_char += rc;
}
return out;
}
template <size_t N>
auto char_star(const char32_t (&in)[N]) noexcept {
mbstate_t state;
constexpr static int out_size = (UTF8_CHAR_MAX_BYTES * N) + 1;
struct final {
char data[out_size];
} out = {{0}};
char* one_char = out.data;
for (size_t rc, n = 0; n < N; ++n) {
rc = c32rtomb(one_char, in[n], &state);
if (rc == bad_size) break;
one_char += rc;
}
return out;
}
} // namespace
#define KATAKANA "片仮名"
#define KATAKANA8 u8"片仮名"
#define KATAKANA16 u"片仮名"
#define KATAKANA32 U"片仮名"
int main(void) {
P("%s", KATAKANA); // const char *
// lot of warnings but ok output
P("%s", KATAKANA8); // const char8_t *
/*
garbled or no output
P( "%s", KATAKANA16 ); // const char16_t *
P( "%s" , KATAKANA32 ); // const char32_t *
*/
setlocale(LC_ALL, "en_US.utf8");
// no can do as there is no standard <cuchar> yet
// P( "%s", char_star(KATAKANA8).data ); // const char8_t *
P("%s", char_star(KATAKANA16).data); // const char16_t *
P("%s", char_star(KATAKANA32).data); // const char32_t *
}
有几件事情发生了变化。 __STDC_UTF_8__
已经不存在,<cuchar>
仍未被"三大编译器"之一实现。
这个链接可能更符合本主题的代码匹配。
std::c8rtomb
和 std::mbrtoc8
尚未提供。
2019年11月
由于未来C++20准备好的编译器还没有提供std::c8rtomb
和std::mbrtoc8
,无法在“三大编译器”中启用执行编码和UTF-8之间的转换。 它们在C++20标准中有描述。
对我来说,c8rtomb()
不是一个“笨拙”的接口,但这可能是主观的。
// g++ prog.cc -std=gnu++2a
// clang++ prog.cc -std=c++2a
#include <stdio.h>
#include <clocale>
#ifndef __clang__
#include <cuchar>
#else
// clang has no <cuchar>
#include <uchar.h>
#endif
#include <climits>
template<size_t N>
void u32sample( const char32_t (&str32)[N] )
{
#ifndef __clang__
std::mbstate_t state{};
#else
mbstate_t state{};
#endif
char out[MB_LEN_MAX]{};
for(char32_t const & c : str32)
{
#ifndef __clang__
/*std::size_t rc =*/ std::c32rtomb(out, c, &state);
#else
/* std::size_t rc =*/ ::c32rtomb(out, c, &state);
#endif
printf("%s", out ) ;
}
}
#ifdef __STDC_UTF_8__
template<size_t N>
void u8sample( const char8_t (& str8)[N])
{
std::mbstate_t state{};
char out[MB_LEN_MAX]{};
for(char8_t const & c : str8)
{
/* std::size_t rc = */ std::c8rtomb(out, c, &state);
printf("%s", out ) ;
}
}
#endif // __STDC_UTF_8__
int main () {
std::setlocale(LC_ALL, "en_US.utf8");
#ifdef __linux__
printf("\nLinux like OS, ") ;
#endif
printf(" Compiler %s\n", __VERSION__ ) ;
printf("\nchar32_t *, Converting to 'char *', and then printing --> " ) ;
u32sample( U"ひらがな" ) ;
#ifdef __STDC_UTF_8__
printf("\nchar8_t *, Converting to 'char *', and then printing --> " ) ;
u8sample( u8"ひらがな" ) ;
#else
printf("\n\n__STDC_UTF_8__ is not defined, can not use char8_t");
#endif
printf("\n\nDone ..." ) ;
return 42;
}
c8rtomb
和 mbrtoc8
函数从 2022-08-02 2.36 版本 开始在 glibc 中可用。 std::c8rtomb
和 std::mbrtoc8
由 libstdc++ 暴露,需要 gcc 12.1 或更高版本(但要求 libstdc++ 构建于 glibc 2.36 或更新版本),并且将由 libc++ 暴露,当 Clang 16 可用时(当 C 库实现在全局命名空间中可用时)。我没有其他 C 或 C++ 标准库实现的信息。 - Tom HonermannVS 2019
ostream& operator<<(ostream& os, const u8string& str)
{
os << reinterpret_cast<const char*>(str.data());
return os;
}
要将控制台设置为UTF-8,请使用https://github.com/MicrosoftDocs/cpp-docs/issues/1915#issuecomment-589644386
这里是应符合C++20标准的代码。由于目前(2020年3月)没有编译器实现论文中定义的转换函数,我决定不受当前实现的限制,而使用完整的C++20规范。因此,我不使用std::basic_string
或std::basic_string_view
,而是使用代码单元的范围。返回值不太通用,但很容易将其更改为输出范围。这留给读者作为练习。
/// \brief Converts the range of UTF-8 code units to execution encoding.
/// \tparam R Type of the input range.
/// \param[in] input Input range.
/// \return std::string in the execution encoding.
/// \throw std::invalid_argument If input sequence is ill-formed.
/// \note This function depends on the global locale.
template <std::ranges::input_range R>
requires std::same_as<std::ranges::range_value_t<R>, char8_t>
std::string ToECSString(R&& input)
{
std::string output;
char temp_buffer[MB_CUR_MAX];
std::mbstate_t mbstate{};
auto i = std::ranges::begin(input);
auto end = std::ranges::end(input);
for (; i != end; ++i)
{
std::size_t result = std::c8rtomb(temp_buffer, *i, &mbstate);
if (result == -1)
{
throw std::invalid_argument{"Ill-formed UTF-8 sequence."};
}
output.append(temp_buffer, temp_buffer + result);
}
return output;
}
/// \brief Converts the input range of code units in execution encoding to
/// UTF-8.
/// \tparam R Type of the input range.
/// \param[in] input Input range.
/// \return std::u8string containing UTF-8 code units.
/// \throw std::invalid_argument If input sequence is ill-formed or does not end
/// at the scalar value boundary.
/// \note This function depends on the global C locale.
template <std::ranges::input_range R>
requires std::same_as<std::ranges::range_value_t<R>, char>
std::u8string ToUTF8String(R&& input)
{
std::u8string output;
char8_t temp_buffer;
std::mbstate_t mbstate{};
std::size_t result;
auto i = std::ranges::begin(input);
auto end = std::ranges::end(input);
while (i != end)
{
result = std::mbrtoc8(&temp_buffer, std::to_address(i), 1, &mbstate);
switch (result)
{
case 0:
{
++i;
break;
}
case std::size_t(-3):
{
break;
}
case std::size_t(-2):
{
++i;
break;
}
case std::size_t(-1):
{
throw std::invalid_argument{"Invalid input sequence."};
}
default:
{
std::ranges::advance(i, result);
break;
}
}
if (result != std::size_t(-2))
{
output.append(1, temp_buffer);
}
}
if (result == -2)
{
throw std::invalid_argument{
"Code unit sequence does not end at the scalar value "
"boundary."};
}
return output;
}
/// \brief Converts the contiguous range of code units in execution encoding to
/// UTF-8.
/// \tparam R Type of the contiguous range.
/// \param[in] input Input range.
/// \return std::u8string containing UTF-8 code units.
/// \throw std::invalid_argument If input sequence is ill-formed or does not end
/// at the scalar value boundary.
/// \note This function depends on the global C locale.
template <std::ranges::contiguous_range R>
requires std::same_as<std::ranges::range_value_t<R>, char>
std::u8string ToUTF8String(R&& input)
{
std::u8string output;
char8_t temp_buffer;
std::mbstate_t mbstate{};
std::size_t offset = 0;
std::size_t size = std::ranges::size(input);
while (offset != size)
{
std::size_t result = std::mbrtoc8(&temp_buffer,
std::ranges::data(input) + offset, size - offset, &mbstate);
switch (result)
{
case 0:
{
++offset;
break;
}
case std::size_t(-3):
{
break;
}
case std::size_t(-2):
{
throw std::invalid_argument{
"Input sequence does not end at the scalar value "
"boundary."};
}
case std::size_t(-1):
{
throw std::invalid_argument{"Invalid input sequence."};
}
default:
{
offset += result;
break;
}
}
output.append(1, temp_buffer);
}
return output;
}
ToUTF8String
重载中,我认为你对 -2
情况的处理不太正确。在那种情况下,temp_buffer
中没有任何值被存储,因此在 switch 语句之后无条件追加到 output
是不正确的。否则,这段代码看起来相当不错! - Tom Honermann
std::cout
没有 UTF-8 版本。 - TrebledJ