最終更新日時(UTC):
が更新

履歴 編集

UTF-8エンコーディングされた文字の型としてchar8_tを追加(C++20)

概要

UTF-8でエンコードされた文字を格納することを想定した型として、符号なし文字型char8_t型を追加する。

char8_t型はunsigned char型と同じ大きさ、アライメント、整数変換順位であるが、独立した型となっており、charunsigned charとはオーバーロードで区別される。

u8プレフィックスの付いた文字/文字列リテラルの型もchar/const char [n]からchar8_t/const char8_t [n]に変更になる。

<string>ヘッダにはstd::basic_string<char8_t>の別名であるstd::u8string型が追加される。同様にして<string_view>ヘッダにはstd::basic_string_view<char8_t>の別名であるstd::u8string_view型が追加される。

std::filesystem::pathクラスのコンストラクタにchar8_t版のオーバーロードが追加され、代わりに必要なくなったstd::filesystem::u8path()関数は非推奨となる。

または破壊的変更として、以下の関数は、戻り値としてcharからchar8_tの文字列を扱うよう変更される:

char系の(ナローマルチバイト)文字列とchar8_t系の(UTF-8)文字列の変換のために、<cuchar>ヘッダにstd::mbrtoc8()/std::c8rtomb()関数が追加される。

ただし、basic_ostream<char>::operator<<()basic_istream<char>::operator>>()に対してchar8_tのオーバーロードは追加されない。これは現状char16_t/char32_t型に対しても存在していないためである。正規表現も同様。

備考

機能テストマクロ__cpp_char8_tで、値は201803

#include <iostream>

template<typename> struct ct;
template<> struct ct<char> {
  using type = char;
};

int main()
{

  const auto *u8s = u8"text";   // u8sの型はC++17まではconst char *だったが、C++20からはconst char8_t *になる
  const char *ps = u8s;         // C++17までは適格だったがC++20からは不適格

  auto u8c = u8'c';             // u8cの型はC++17まではcharだったが、C++20からはchar8_tになる
  char *pc = &u8c;              // C++17までは適格だったがC++20からは不適格

  std::string s = u8"text";     // C++17までは適格だったがC++20からは不適格

  void f(const char *s);
  f(u8"text");                  // C++17までは適格だったがC++20からは不適格

  ct<decltype(u8'c')>::type x;  // C++17までは適格だったがC++20からは不適格
}

出力

この機能が必要になった背景・経緯

C++の元になったC言語がISOで標準規格になる前から文字を格納する型としてchar型ないしint型が存在した。C++もこれを整理しつつ受け継いだ。

一方で8bitでは文字が収まらない文字エンコードも複数登場していた。日本語UNIX環境の開発から生まれたDEC漢字、その後Unixで普及したEUC、そしてUnicodeである。

C言語が初めて標準化された1989年、まだUnicodeはこんにちほど普及しておらず、どの文字エンコードが広く普及するのか、あるいは統一されることはないのか、見通すことはできない状況にあった。

結果としてANSI C89/ISO C90ではwchar_t型を導入するものの、どのようなエンコードを扱うかは未規定とされた。C++98もこれを継承した。

2001年、Unicode側からutf16_t型を追加する提案があった。UTF-16に絞っているのはメモリー効率が良いこと、すでに当時、WindowsやJava、データベースがUTF-16に対応しており、UTF-16を保証する型が必要とされたからであった。これは採用されなかった。

その後絵文字の普及なども後押ししてUnicodeが世界中に普及した。

C++11ではchar16_t/char32_t型が追加された。しかしこの時UTF-8を保証するchar8_t型は提案されなかった。下に示す江添亮氏の解説によればUTF-8はchar型に格納すればよろしい、という考えによるものだ。

本の虫: C++標準化委員会の文書: P0370R1-P0379R0

C++11のときにchar8_tが必要だと訴えたら、charは古典的にバイト列を表現する型なので十分だ。char型以外の型があるのは混乱する。などと理解のないUnicodeの世界に生きていない名だたる委員達から散々に批判された。

2017年11月にW3Techsによって行われた調査によれば90%を超えるWebサイトの文字エンコードにUTF-8が用いられるようになった。

一方でC++でUTF-8を扱うには問題があった。UTF-8のcode unitの値域は128 (0x80)から255 (0xFF)の範囲 (8ビット目) にも及んでいる一方で、C++のchar型は符号の有無が未規定である。そのため、次のコードは意図した挙動を示さない可能性がある。

#include <iostream>

bool is_utf8_multibyte_code_unit(char c) {
  return c >= 0x80;
}

int main()
{
  std::cout << std::boolalpha << is_utf8_multibyte_code_unit(u8"あ"[0]) << std::endl;// => trueにならない可能性がある
}

この問題を回避するため、UTF-8の8ビット目の範囲を扱う必要がある場合は、static_castで符号なし文字型に変換して扱わなければならなかった。

#include <iostream>

bool is_utf8_multibyte_code_unit(char c) {
  return static_cast<unsigned char>(c) >= 0x80;
}

int main()
{
    std::cout << std::boolalpha << is_utf8_multibyte_code_unit(u8"あ"[0]) << std::endl;// => true
}

またC++11で文字列リテラルに対して、C++17で文字リテラルに対してu8プレフィックスが使えるようになり、これはUTF-8でエンコードされることを保証したが、その文字型としては依然としてchar型が使われた。char型ではどのようなエンコードの文字が格納されているか型レベルで判断できず、例としてC++17で追加されたファイルシステムライブラリのpathクラスでは、UTF-8でエンコードされたパス文字列を受け取るためにコンストラクタと代入演算子でオーバーロードができず、u8path()という関数を追加せざるをえなかった。

UTF-8の利用が広く利用されていく中で、C++でもUTF-8を扱う上で障害となる仕様を改める必要があった。そのためにchar8_t型が必要となった。

検討されたほかの選択肢

N3398提案では以下のようにchar8_t型をunsigned char型の別名にすることが提案されていた。

typedef unsigned char char8_t;

以下のようにenum classを利用する選択肢もあったが、P0372R0提案はchar8_t型を使うためにヘッダのインクルードが必要になることは望ましくないと述べている。

enum class char8_t : unsigned char {};

関連項目

参照

char8_t型を追加する提案文章

その他