将std::u8string和std::string之间进行转换

36

C++20新增了char8_tstd::u8string用于UTF-8编码。但是,没有UTF-8版本的std::cout和操作系统API大多数只接受char和执行字符集,所以我们仍然需要一种方法在UTF-8编码和执行字符集之间进行转换。

我重新阅读了一篇char8_t论文,看起来唯一的在UTF-8编码和执行字符集之间转换的方法是使用std::c8rtombstd::mbrtoc8函数。然而,它们的API非常令人困惑。有人可以提供一个示例代码吗?


1
然而,std::cout 没有 UTF-8 版本。 - TrebledJ
9
这句话使用了广泛的执行字符集。 - user3624760
4
在Mac和Linux上,通常可以直接打印UTF-8(我认为有一些罕见的Linux发行版不适用此方法)在Windows上,您应该转换为“wchar_t”并使用“wcout”。 - Alan Birtles
7
我希望保持跨平台性,所以我不能假设什么是ECS。我的主要平台是Linux,因此在实际应用中,我自己可以进行无损转换,但我仍然希望保持跨平台的范围内。 - user3624760
6
我听说Windows 10对UTF-8的支持有所改进 - 有些。微软现在允许UTF-8用作用户的ANSI区域设置(但该功能目前仍处于测试版),这样您就可以在ANSI API中使用UTF-8字符串。但操作系统和Unicode API仍然基于UTF-16,为获得最佳性能,您应坚持使用UTF-16。 - Remy Lebeau
显示剩余4条评论
7个回答

38
在C++20中,对UTF-8的"支持"似乎是一个糟糕的玩笑。
标准库中唯一支持UTF的功能是对字符串和字符串视图的支持(std::u8string,std::u8string_view,std::u16string等)。仅此而已。标准库不支持在正则表达式、格式化、文件I/O等方面对UTF编码的支持。
在C++17中,你至少可以将任何UTF-8数据轻松地视为'char'数据,这使得可以在不损失性能的情况下使用std::regex、std::fstream、std::cout等。
在C++20中,情况将发生变化。例如,你将无法再写出std::string text = u8"..."; 这样的代码。
std::u8fstream file; std::u8string line; ... file << line;

由于没有`std::u8fstream`,所以无法实现。
即使是新的C++20标准中的`std::format`也完全不支持UTF,因为所有必要的重载都被简单地省略了。你无法编写
std::u8string text = std::format(u8"...{}...", 42);

更糟糕的是,std::stringstd::u8string之间没有简单的转换(或者说转换)。甚至const char*const char8_t*之间也没有。因此,如果你想要格式化(使用std::format)或者输入/输出(std::cinstd::coutstd::fstream,...)UTF-8数据,你必须在内部复制所有字符串。- 这将是一个不必要的性能杀手。
最后,如果没有输入、输出和格式化,UTF有什么用呢?

1
printf("%s", (char const *)u8"ひらがな"); -- 这可以用于打印。目前为止。 - Chef Gladiator
2
@ChefGladiator 这样做了吗?你怎么知道 printf 另一端的程序实际上将你的流解码为 utf8? - spectras
1
@spectras 我的意思正是如此。明确地说,我非常清楚编程语言不会基于假设、黑客技巧或未记录的编译器行为工作。 - Chef Gladiator
2
@ChefGladiator,“charN_t”类型的命名非常糟糕,因为它们与“字符”毫无关系。将Unicode标量值映射到字形也非常复杂。我正在制定一份具有适当名称和强类型的建议 - user3624760
1
真遗憾utf8不能直接集成到std :: string中。这是我更喜欢Qt而不是标准库(和STL)的许多原因之一。 - Kiruahxh
显示剩余4条评论

6
目前,标准提供的仅有的接口是 std::c8rtombstd::mbrtoc8,它们允许在执行编码和 UTF-8 之间进行转换。这些接口比较笨拙,它们是为了匹配预先存在的接口(如 std::c16rtombstd::mbrtoc16)而设计的。对于这些新接口增加到 C++ 标准中的措辞故意与现有相关函数的 C 标准措辞相匹配(希望这些新函数最终也能被添加到 C 中;我仍然需要追求这一点)。匹配 C 标准措辞的目的是确保任何熟悉 C 措辞的人都会认识到 char8_t 接口的工作方式相同。
cppreference.com 上有一些针对这些函数的 UTF-16 版本的示例,应该对理解 char8_t 变体很有用。

与此同时,cppreference已经添加了完整的<cuchar>规范,作为每个C++20标准库都应该包含的内容。 - Chef Gladiator
1
c8rtombmbrtoc8接口也已经通过N2653的采用于C23中添加,如记录在1月/2月WG14虚拟会议的会议记录N2941中所示。 - Tom Honermann
啊,汤姆回来了。两三年后,但他确实回来了。欢迎,汤姆。 - Chef Gladiator

4

C++权威人士在每年的CppCon会议上(例如2018和2019年)通常给出的答案是应该选择自己喜欢的UTF8库。有各种各样的风格,只需选择您喜欢的即可。目前C++方面对Unicode的支持和理解还非常有限。

有些人希望C++23中有相关的改进,但目前我们甚至没有一个官方的工作组。


1
我们甚至“没有一个官方的工作组”,就像已经存在了一年以上的SG16那样? - Davis Herring
4
真是个笑话。如果我们只能得到一个简陋不完整的草稿,还会破坏现有的代码,那么一开始为什么要将它加入标准中呢?请问需要翻译的内容是:What a joke. Why add it in the standard then in the first place if we only get a rudimentary incomplete draft which breaks existing code? - Sebastian Hoffmann
现在距离C++23只有4个月了,我可以向你保证,不会有任何改进。希望能在C++26或C++29中看到改进...或者永远也不会有。 - Lothar
显然,根据昨天@Tom Honnerman的评论,C++23将完全实现utf8(这是我的理解)。PS:标准C23也将具有此功能。 - Chef Gladiator

4
据我所知,目前C++还没有提供这种转换的工具。然而,我建议不要首先使用std::u8string,因为它在标准中得到了很差的支持,并且任何系统API都不支持它(由于兼容性原因,这很可能永远不会得到支持)。在大多数平台上,普通的char字符串已经是UTF-8格式,在Windows平台上使用MSVC编译时可以添加/utf-8选项,在主要操作系统上获得可移植的Unicode支持。

3

2022年11月20日更新

如果你还在为utf8和C++而苦恼,那么好消息是,在等待多年后,我们终于迎来了utf8的春天(2022年11月)。请参见下面Tom的评论。

我认为在短短几个月内,“3”都将实现utf8。有点运气的话,C++23将最终包含完整的utf8实现。正如早在C++11时所宣传的那样。

刚刚检查了这台机器上的cl.exe(版本号19.34.31933),没有标准的,但已经完全实现了。

2022年4月19日更新

// 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 *
}

2021年3月19日更新

有几件事情发生了变化。 __STDC_UTF_8__ 已经不存在,<cuchar> 仍未被"三大编译器"之一实现。

这个链接可能更符合本主题的代码匹配。

2020年3月17日更新

std::c8rtombstd::mbrtoc8 尚未提供。

2019年11月

由于未来C++20准备好的编译器还没有提供std::c8rtombstd::mbrtoc8,无法在“三大编译器”中启用执行编码和UTF-8之间的转换。 它们在C++20标准中有描述。

对我来说,c8rtomb()不是一个“笨拙”的接口,但这可能是主观的。

WANDBOX

//  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;
}

我已经注释并记录了今天无法编译的代码行。

1
c8rtombmbrtoc8 函数从 2022-08-02 2.36 版本 开始在 glibc 中可用。 std::c8rtombstd::mbrtoc8 由 libstdc++ 暴露,需要 gcc 12.1 或更高版本(但要求 libstdc++ 构建于 glibc 2.36 或更新版本),并且将由 libc++ 暴露,当 Clang 16 可用时(当 C 库实现在全局命名空间中可用时)。我没有其他 C 或 C++ 标准库实现的信息。 - Tom Honermann
一个有争议的话题;在过去的几天里,我得到了+10和-2(笑) - Chef Gladiator

0

-1

这里是应符合C++20标准的代码。由于目前(2020年3月)没有编译器实现论文中定义的转换函数,我决定不受当前实现的限制,而使用完整的C++20规范。因此,我不使用std::basic_stringstd::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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接