UTF-8 と Latin 1、文字集合に関して

この記事は Qt Blog の "On UTF-8, Latin 1 and charsets" を翻訳したものです。
執筆: Thiago Macieira 2011年3月26日

昨日、私は QString のデフォルトコーデックを Latin 1 から UTF-8 に変更することについて、実現の可能性を探る調査の 記事 を書きました。実は昨日の記事のために用意した原稿は非常に長かったので、内容を実際のコードとパフォーマンスに絞り、その背景や合理性、詳細については今回の記事のために残しておきました。

昨日の記事を最初に抜粋します。

しかし、2011 年にもなってなぜ ASCII に限定する必要があるのか私は疑問に思いました。つまり、英語で翻訳前のメッセージを書く場合であってもマイクロ(μ)や度(°)、コピーライト(©)、さらにはユーロ記号(€)などの ASCII の範囲外の文字を必要とすることがあるでしょう。特に、最後のユーロ記号は意図的に例示しました。それはその他の文字とは違って Latin 1 にさえ含まれていないからです。ユーロ記号を表示するためには別の文字コードを使用する必要があります。これに加えて、2011 年現在、テキストをやり取りする際のデファクトスタンダードとなるエンコードは UTF-8 です。

背景: C++ の標準に関連する文字集合

C と C++ の標準では2つの文字集合について述べられています。ソースの入力文字集合と実行文字集合です。GCC のマニュアルにはさらに2つを加えた、4つの文字集合が記載されています。1つは(ワイド文字の大きさに依存して UTF-16 もしくは UCS-4 となる)ワイド文字の文字集合で、もう1つはコンパイラが内部で使用する文字集合です。ここでの目的は最初の2つを結びつけることです。

ソースの入力文字集合とはソースファイルの文字コードのことです。C 言語の初期には ASCII 以外にも(EBCDIC 等の)様々な文字集合が使用されていたため、コンパイラが入力文字集合を適切に判断し処理する必要がありました。入力文字集合を間違えて扱った場合には、あるバイトがスペースや改行なのかさえ理解できなくなるからです。今日でも入力文字集合を ASCII だと決めつけてこの問題を避けるようなコンパイラを書く人はいるかもしれません。入力文字集合はコンパイラがファイルをメモリにロードし解析可能な形式に変換する際に使用されます。

実行文字集合はコンパイラがオブジェクトファイルを生成する際に埋め込む文字列に使用するものです。"Résumé" のように英語としても使われるようになった単語を書いた場合、コンパイラは “é” をエンコードする方法を探す必要があります。コンパイラはソースファイルをメモリ上にロードし、コンパイル前に内部の形式に変換します。ここではコンパイラがこの文字を LATIN SMALL LETTER E WITH ACUTE だと理解していると考えます。ディスク上で “é” がどうであるかはこのブログ記事のエンコードとは無関係です。

GCC のマニュアルによると、デフォルトの実行文字集合が UTF-8 なのに対して、デフォルトの入力文字集合はロケールの文字集合になっています。この説明は完全に正しいわけではありません。文字集合を設定しない場合、GCC は入力されたものと全く同じバイトを出力します。これは Latin 1 でエンコードされたファイルを UTF-8 ロケール上でコンパイルすることで簡単に確かめることができます。そして予想通りこれは動きます。この動作を変更した場合には非常に多くのプログラムが動作しなくなるので、GCC の開発者はこの変更をしないのでしょう。しかし、 -finput-charset=-fexec-charset= を指定した場合、それがデフォルトと同じであったとしても、不正にエンコードされたものが見つかった場合には GCC は逃げ出すでしょう。

(訳注: この元記事が書かれた日の)1、2週間前にこれについての議論を Freenode の #qt の IRC チャンネルで行いました。ある開発者が QString が文字列リテラルをデコードする際に実行文字集合の代わりに Latin 1 を使用している理由を尋ねたことが始まりでした。彼は “ü” という文字を単純に "u00fc" と記述できない理由についても尋ねました。答えはシンプルで以下の2つの理由があります。

  1. Qt と QString はコンパイル時にどの実行文字集合が選ばれるかを知りません
  2. 実行文字集合は1つではありません。1つのオブジェクトファイルは他のオブジェクトやライブラリに由来する異なる文字集合を含むことができます

QString の現状

QString の実際の処理を見てみると、実行文字集合の変更をサポートしていることが分かるでしょう。私が Latin 1 がデフォルトだと言う裏には、それが変更可能であることが隠されています。[qt QString] のドキュメント では、const char * を引数に取る全ての関数は [qt QString fromAscii] を参照しています。この関数の名前は実は正確ではなく、必ずしも ASCII からの変換を行うわけではありません。ドキュメントには "コーデックによっては US-ASCII(ANSI X3.4-1986)の有効な入力を受け付けないことがあります" と実際に書いてあります。

この関数が fromAscii と呼ばれているのは現在のほとんどのソースコードが ASCII で書かれているからです。この関数は 2002 年にリリースされた Qt 3 で登場しました(当時の ドキュメント)。当時は現在ほど UTF-8 が広く普及していませんでした。私が自分の Linux デスクトップを UTF-8 に変えたのは 2003 年だったのを覚えています。非 ASCII のバイト列を含むファイルは、同じ国の人同士では多くの場合で問題なく交換できましたが、他の国の人に送った場合には高い確率で文字化けするという状況でした。

アプリケーションを開発している小さなチームが、私が以前に挙げたような "度" や "コピーライト" 等の非 ASCII 文字を使用することはしばしばありました。これに対応するために QString では文字リテラルをデコードするためのコーデックを変更できるようになっているのです。

言い方を変えると、[qt QTextCodec::setCodecForCStrings] を使用することで、実行文字集合を Qt に教えることができるのです(前述の1番目の問題)。しかし2番目の問題に対してできることは何もないため、ライブラリはそれぞれの関数でどのコーデックに含まれる文字列なのかを Qt に教えなければならないのです。

C++0x での (部分的な) 解決方法: Unicode リテラル

2011 年時点でもいまだに C++0x と呼ばれる C++ 言語の次世代の標準規格には、新たな文字列リテラル と呼ばれる、文字列が UTF の文字集合のどれかでエンコードされていることを保証するための新しい記述方法が含まれます。つまり以下のようにコードを書けるということです。

    u8"I'm a UTF-8 string."
u"This is a UTF-16 string."
U"This is a UTF-32 string."

これを受け取る側の QString ではこれらの文字コードがそれぞれ間違いなく UTF-8、UTF-16 もしくは UTF-32 であることが分かるでしょう。UTF-8 でエンコードされた文字列は、既存の文字列リテラルと同じように const char[] にとして扱われるため、QString ではこの違いを見分けることはできません。しかし、他の2つの新しい型 const char16_t[]const char32_t[] はオーバーロードによって使い分けることが可能で、その文字列を完璧にデコードすることが可能です。

これにより、IRC で尋ねてきた開発者は安心して u"u00fc" と書くことができ、QString がそれを LATIN SMALL LETTER U WITH DIAERESIS(U+00FC) としてデコードすることは保証されます。

私は C++0x 委員会がこの問題を一部しか解決しなかったことに不満を持っています。私は u"Résumé" と書いたファイルを(Windows などの)他のプラットフォームを使っている同僚に送れるようになることを望んでいます。さらに彼のコンパイラが私のソースコードを私が意図した通りに解釈して動作してくれることを望んでいます。つまり、私はソースコードを UTF-8 で保存するので、全てのコンパイラに UTF-8 をソースの入力文字集合として使用してほしいということです。

C++0x 委員会はこのような決定をしなかっただけではなく、私のソースファイルをこのようにマークする方法も除かれました。このため、ソースファイルのデコードは完全にコンパイラの設定に依存したままです…。

私の提案するソリューション

自分のソースコードの文字コードが何かをコンパイラに伝える方法がないため、QString を効率よく作成する方法で妥協することにしました。QString が内部的にデータを UTF-16 で保持することは今後も変わりません。つまりコンパイラにソースコードのリテラルを UTF-16 に変換させる必要があるということです。これは、C++0x の新しい文字列リテラルを使用することで可能になります。アプリケーションの実行時にこれらの文字列はメモリ上にロードされますが、読み取り専用でアンロードされることがないため、以下のようにすることが可能です。

<tt>    QString s = QString::fromRawData(u<span style="color: #bf0303">"Résumé"</span>);</tt>

どうでしょうか。ここで “é” をコンパイラが間違って解釈するかもしれないので、以下のようにさらに妥協する必要があるでしょう。

<tt>    QString s = QString::fromRawData(u<span style="color: #bf0303">"Ru00e9sumu00e9"</span>);</tt>

これでもまだ私の感覚では少し冗長です。昨日の記事で、これを行うためのマクロを 提案 してくれた人がいました。しかし、別の C++0x の機能 ユーザー定義リテラル (こちらの 定義 もあわせて参照してください)を使用すると、以下の演算子を定義することができます。

<tt>QString <strong>operator</strong> "" q(const char16_t *str, size_t len);</tt>

これにより以下のような記述が可能になります。

<tt>QString s = u<span style="color: #bf0303">"Résumé"</span>q;</tt>

違和感はありますが、見栄えはとてもいいです。残念ながら 最新の GCC のリリース でも、この機能はまだ 未実装 となっています。

追記: C++ 標準化委員会が 2011 年 3 月に行われた会議において C++ 言語の 国際標準の最終ドラフト を承認したという記事を Herb Sutter が 彼のブログ に書いたことを友人から教わりました。今年の夏には投票が行われ、C++ 2011 となるでしょう。


Blog Topics:

Comments