C++11 で Unicode プログラミングのススメ このエントリーをはてなブックマークに追加

このエントリは、C++11 Advent Calendar 2011 への参加記事です。

初心者表明を免罪符にするつもりは毛頭無いのですが、 C++0x/11 の学習、およびそれを用いた経験はまだまだ浅いため、内容的に拙い部分が多々あることを、あらかじめご容赦願いたいと思います m(_ _)m 。ていうか突っ込みだいかんげいでつ。

一応 ISO/IEC 14882:2011 の draft “n3242″ を参照しています。 GCC は 4.7 入れるの面倒だったので、動作確認できるものについては Ubuntu 11.10 に入っていた 4.6.1 を用いています。

Unicode に対応したリテラル

文字リテラルについてはドラフトの 2.14.3、文字列リテラルについては 2.14.5 に記述があります。

文字リテラルには従来の

'a'
L'あ'

といったスタイルに加えて、

u'\u00a9'   // コピーライト記号
U'\U0002000b'  // 丈の右上に点がついた字

といったスタイルが追加されました。想定されるべき対応関係を表にすると以下の通りになります。

記述スタイル 文字セット 物理型
'' 所謂 C 文字。
マルチバイトの 1 オクテットでもいいし、まぁ、何でもあり。
char
l'' または L'' ユニバーサル文字セット (UCS)。 wchar_t
u'' UTF-16 char16_t
U'' UTF-32 char32_t

文字列リテラルではさらに u8 という接頭子も使えます。

u8"Copyright \u00a9 2011 Harapeko, Inc."    // \u00a9 は UTF-8 のオクテット列 [C2 A9] に変換される
u"\U0002000bは「丈」の字にクリソツ"         // \U0002000b は UTF-16 の該当するサロゲートペアに変換される…ハズ

対応関係の表は、…面倒くさいからもういいか。

あとさらっと流しちゃいましたが、 Unicode 用のエスケープ文字も追加されました。\uNNNN は 16bits の、 \UNNNNNNNN は 32bits の UCS を表現できます。上記の例のように、適切な文字列リテラル内で使用すれば、対応する文字セットの数値列に適宜変換されるはずです。この辺の説明はドラフトの 2.3 にありますが、以下の説明の通り、あくまで UCS の文字値を表現するものであって UTF の数値列を表現するものではないので、 \uNNNN の形式でサロゲートペアの上位代用符号位置に相当する値を指定することはできません。

The character designated by the universal-character-name \UNNNNNNNN is that character whose character
short name in ISO/IEC 10646 is NNNNNNNN; the character designated by the universal-character-name \uNNNN
is that character whose character short name in ISO/IEC 10646 is 0000NNNN. If the hexadecimal value for a
universal-character-name corresponds to a surrogate code point (in the range 0xD800.0xDFFF, inclusive),
the program is ill-formed. Additionally, if the hexadecimal value for a universal-character-name outside the
c-char-sequence, s-char-sequence, or r-char-sequence of a character or string literal corresponds to a control
character (in either of the ranges 0×00.0x1F or 0x7F.0x9F, both inclusive) or to a character in the basic
source character set, the program is ill-formed.15

Unicode に対応した物理型

Unicode に対応した型については、ドラフトの 3.9.1 に説明があります。重要なのは多分以下の箇所。

  1. Type wchar_t is a distinct type whose values can represent distinct codes for all members of the largest
    extended character set specified among the supported locales (22.3.1). Type wchar_t shall have the same
    size, signedness, and alignment requirements (3.11) as one of the other integral types, called its underlying
    type. Types char16_t and char32_t denote distinct types with the same size, signedness, and alignment as
    uint_least16_t and uint_least32_t, respectively, in <stdint.h>, called the underlying types.

エーゴは苦手なんですが、ここを読む限り、wchar_t はサポートするロケールに含まれるもっとも大きな値の文字値を表現できるのに十分なサイズの整数型であることが補償されてなきゃいけなさそうに見えます。 wchar_t については大分昔に見捨てているんですが (^_^; 、VC++2010 だと 32bits 整数に変更されていたりするんでしょうか…?

# この辺とか見る限り、やっぱり unsigned short int 相当、のままみたいですね… orz

char16_tchar32_t は、それぞれ UTF-16、 UTF-32 を扱うための型と考えて差し支えなさそうです。

「内部文字」のポリシー

型についての想定を考えるならば、プログラムが内部で扱う文字データは、 C++11 では wchar_t を使用するべきであるように思われます。将来的にはそうなってゆくべきなのでしょう。しかし過去との互換性などの観点から、各ベンダーの wchar_t に対する取り扱いは当面現状維持か、もしくは段階的な仕様変更 (コンパイラオプションでの切り替え等) となっていくことが予想されます。

それに対し、 UTF-32 に関して言えば、恐らく向こう十何年かぐらいは「1要素 = UCS 1文字」であり続けるのではないかと思われます。従って、内部文字への要件として「1文字を 1つの数値のみで扱いたい」というのがあるのであれば、当面は char32_tU"" 形式のリテラルを用いるのが良さそうです。

要件 選択すべき型と文字セット
  • 1文字を 1つの数値のみで扱いたい
  • メモリー使用量は気にしないか、32bits 幅でも十分管理可能
char32_t、 UTF-32
  • <(boost/)regex> を使いたい (後述)
  • UTF-8 のクセに精通しているのでマルチバイトでも気にならない
  • メモリー使用量を極力抑えたい
char、 UTF-8
  • とにかく wchar_t を使い慣れている
  • 数十年後の未来との互換性、汎用性に賭けたい
wchar_t、 UCS

char32_t で文字列置換を試してみる

そんなわけで、実際に UTF-32 を内部文字の文字セットとして採用したプログラム例を作ってみることにしました。内容的には、静的に用意した文字列内のすべての「くま」を「ぱんだ」に置き換える、という簡単なものです。

#include <iostream>
#include <algorithm>
#include <string>

using namespace std;

int main()
{
    u32string before = U"てくまくまやこんてくまくまやこん むらやましゃちょうよ おおきなくまにな~ぁれ";
    u32string after;
    u32string kuma = U"くま";
    u32string panda = U"ぱんだ";

    auto start_it = before.begin();
    auto find_it = start_it + (kuma.size() - 1);
    while (find_it < before.end()) {
        int cnt = 0;
        auto stop_it = find_if(kuma.rbegin(), kuma.rend(), [&cnt, find_it](char32_t letter) {
            return *(find_it - cnt++) != letter;
        });
        if (stop_it != kuma.rend()) {
            find_it += cnt;
            continue;
        }
        // くまを発見、ぱんだに変身!!
        after.append(start_it, find_it - (kuma.size() - 1));
        after += panda;
        start_it = find_it + 1;
        find_it = start_it + (kuma.size() - 1);
    }
    after.append(start_it, find_it);

    cout << "before: " << before.size() << endl;
    cout << "after: " << after.size() << endl;

    return 0;
}

えっと… アルゴリズムの説明とかはいいですよね? 文字列の先頭からと検索語の後からで評価して、完全一致しなかった場合は一致した数値の数だけ読み飛ばして、を繰り返すというオーソドックスなやり方です。これだったらかっこつけて find_if とか使わんで for で回しても大して変わらんやんとかそういう突っ込みはさておき (^_^;

GCC4.6/Ubuntu での実行結果は以下の通り。

murachi@ubuntu-vbox:~/otoco/trunk/sample$ g++ -std=c++0x -o kumapan-n kumapan-n.cpp 
murachi@ubuntu-vbox:~/otoco/trunk/sample$ ./kumapan-n
before: 39
after: 44
murachi@ubuntu-vbox:~/otoco/trunk/sample$ 

実行結果として置換前後の u32string::size() を表示しています。 5つある「くま」が「ぱんだ」に置き換えられたので、その数が 5 増えています。増える筈の文字数と一致するので、正しく動作しているように見えます。

iconv を使って実際に出力してみる

さて、実際の文字列を出力してみたいのですが、このままだとロケールが UTF-8 で動作している端末上では表示できません。ファイルに出力してテキストエディタで、という手もありますが、せっかくなので libiconv を使って指定した文字セットに変換して出力、ということをやってみることにしましょう。

libiconv の利用に際しては、お手製のラッパークラスを作成することで対応しました。作成したソースコードを以下にリンクします。

このクラスは過去の記事においても使用しておりますが、 C++11 の勉強も兼ねて (?)、内部文字に使用する物理型と文字セットをテンプレートパラメータに指定できるテンプレートクラスに書き換えています (あ、過去の記事でのソースへのリンク先が最新版になっちゃってる…直さなきゃ…)。

そして先ほどのサンプルプログラムは、最初の方で EncodeString.hpp#include し、

#include <iostream>
#include <algorithm>
#include <string>

#include "EncodeString.hpp" // ←

using namespace std;

最後の方で出力内容を以下のように修正します。

    cout << "before: " << EncodeString<char32_t, chset_Utf32>(before, chset_Utf8).getCharArray() << endl;
    cout << "after: " << EncodeString<char32_t, chset_Utf32>(after, chset_Utf8).getCharArray() << endl;

Windows 環境とかで Shift JIS (CP-932) で出力したい人は、 chset_Utf8chset_Cp932 に置き換えてあげれば ok です。GCC4.6/Ubuntu での実行結果は以下の通り。

murachi@ubuntu-vbox:~/otoco/trunk/sample$ g++ -std=c++0x -o kumapan EncodeString.cpp kumapan.cpp 
murachi@ubuntu-vbox:~/otoco/trunk/sample$ ./kumapan
before: てくまくまやこんてくまくまやこん むらやましゃちょうよ おおきなくまにな~ぁれ
after: てぱんだぱんだやこんてぱんだぱんだやこん むらやましゃちょうよ おおきなぱんだにな~ぁれ
murachi@ubuntu-vbox:~/otoco/trunk/sample$ 

環境によっては libiconv を別途導入してコンパイルオプションに -liconv を付け加える必要があるかもしれません (MinGW とか←動作未確認)。

正規表現を使いたい

さて、上記のサンプルでさらっと u32string とか使っちゃってますが、このシノニムはドラフトの 21.3 にてちゃんと明記された標準のものです。もちろん、 u16string というのも存在します (u8string は無いので、考慮されているのはアラインメントのみと考えるべきですが…)。

しかし、「28 Regular expressions library」の章においては、 char32_t という文字はカケラも hit しません。標準の <regex> においては、 char16_tchar32_t への対応は見送られてしまっているようです。

もちろん、basic_regex はテンプレートクラスなのですから、自分でテンプレートパラメータを指定してあげればうまくいきそうに見えます。しかし、同様の試みを Boost.Regex について行った際には、 std::bad_cast 例外が送出されてプログラムがエラー終了してしまいました。将来的には、あるいは処理系によってはうまく動かせる (ようになる) のかもしれませんが、あまり期待は持たない方が良いかもしれません…。

# そもそも GCC (libstdc++) では <regex> 自体がまだちゃんと実装されてなかったり… orz

もっとも、Boost.Regex の ICU 拡張における UChar32char32_t (およびそれらの配列へのポインタ) を無理矢理キャストして使うと割と上手く行くっぽかったりするので、どうにかこうにかラッパーを書いて当座はそれで凌ぐというのも手かもしれません…。

ちなみに、char と UTF-8 を使用するのであれば <regex> はそのまま使えるはずですが、その場合、 (Boost.Regex と同様に) <regex> は UTF-8 を知らないので、マルチバイト特有の問題に悩まされることになるでしょう。少なくとも日本語の文字に対する量指定子 (あ+ とか あ? とか) は期待通りには動きません。

仮に、<regex>char32_t で利用できる場合、先のサンプルは以下のようなコーディングになるでしょう。こういう風に組める日がいつか来るといいですね… (;_;)/

#include <iostream>
#include <algorithm>
#include <string>
#include <regex>

#include "EncodeString.hpp"

using namespace std;

typedef basic_regex<char32_t, regex_traits<char32_t>> u32regex;
typedef match_results<u32string::const_iterator> u32smatch;


int main()
{
    u32string before = U"てくまくまやこんてくまくまやこん むらやましゃちょうよ おおきなくまにな~ぁれ";
    u32string after;
    u32regex reg(U"くま");
    u32smatch match;

    u32string textbuf = before;
    while (regex_search(textbuf, match, reg)) {
        after += match.prefix().str() + U"ぱんだ";
        textbuf = match.suffix().str();
    }
    after += textbuf;

    cout << "before: " << EncodeString<char32_t, chset_Utf32>(before, chset_Utf8).getCharArray() << endl;
    cout << "after: " << EncodeString<char32_t, chset_Utf32>(after, chset_Utf8).getCharArray() << endl;

    return 0;
}

2011 年 12 月 27 日 by 村山 俊之

タグ: , , , , , , , ,

コメント / トラックバック 1 件

  1. はらぺこ日誌» ブログアーカイブ » unorderd_map のキーに enum 型を使用する より:

    [...] はらぺこ日誌 株式会社はらぺこ 公式ブログ « C++11 で Unicode プログラミングのススメ [...]

コメントをどうぞ