初投稿です。
はじめに
UTF-8 なバイト配列にバイトだけでなく直接 Unicode のコードポイントを追加・削除できたり、iterator をstd::regex
とか<algorithm>
にそのまま使えたりしたらいいな、と思ったので、std::basic_string
に倣って rope を書いています。ソースは GitHub で保管してます。もうちょっと書いたら push します。
色々今までよく分からなかった C++ のことが、これを書いてるのをきっかけに少し分かるようになった(つもりな)ので、何枚か記事を書いてまとめたいと思います。誰かの役に立てば嬉しいですね。
何をしたかったか
std::basic_string<CharT>
のメソッドの例をいくつか挙げます。
basic_string(
size_type count,
CharT ch,
const Allocator& alloc = Allocator()
);
basic_string& operator=(
CharT ch
);
basic_string& assign(
size_type count,
CharT ch
);
当たり前のことですが、std::basic_string<CharT>
のメソッドの引数は、CharT
がベースです。しかし、バイトもコードポイントも書き込める rope を実装するには、引数をバイトとするメソッドと、引数をコードポイントとするメソッドの両方が欲しいですね。これを実装するには色んな方法があるのですが、そのうち筆者が思いついたものを挙げていきます。
- 違う関数名で、引数をバイトとする関数と引数をコードポイントとする関数を書く
- template で全部拾って、引数としてバイトを渡しているのか、コードポイントとして渡しているのかを伝える「モード」的な引数を設ける
- 同じ関数名でバイトの型とコードポイントの型のオーバーロードを書く
- template で全部拾って、ある型に特化したコードポイント版を書き、汎用はバイト版として書く
それぞれの書き方と、良い点と悪い点を考えていきます。
別々の関数を書く
std::basic_string::assign
に倣って、rope::assign
をこの方法で書いてみるとしましょう。実装はこんな感じになります。
rope& rope::assign(
size_type count,
OctetT ch // OctetT はバイトを表す型
) {
// chをバイトとして何らかの処理を行う
}
rope& rope::assign_cp(
size_type count,
CodepointT ch // CodepointT はコードポイントを表す型
) {
// chをコードポイントとして何らかの処理を行う
}
ユーザからしても、関数名からこの関数がバイトなのかコードポイントなのかが分かるので、筆者はこの方法を使っています。
しかし、コンストラクタのようなメソッドは、関数名を指定できないので、この方法は使えません。別の方法を考えましょう。
template と「モード」引数
rope のコンストラクタを実装すると、こんな感じになります。
enum struct mode {
octet, cp
};
template <typename GeneralT>
rope(
size_type count,
GeneralT ch,
const mode& m = mode::octet, // デフォルトはバイト
const Allocator& alloc = Allocator()
) : /* 初期化 */ {
if (m == mode::octet) {
// chをバイトとして何らかの処理を行う
}
else {
// chをコードポイントとして何らかの処理を行う
}
}
template を使っているので、色々な型を受け付けながらも、引数がバイトを表しているのか、コードポイントを表しているのかを指定できるというのは便利ですね。引数がconst T*
だったり、std::initializer_list<T>
だったりすると、キャストがややこしくなるので、template を使うのは適策だと思います。
しかし、operator をオーバーロードする場合は、引数の量を増やすことはできません。operator は、標準ライブラリの<algorithm>
とかに対応するためには不可欠なので、別の方法を考えないといけません。
同じ関数名で overload する
operator=
を実装してみましょう。
rope& operator=(OctetT ch) {
// chをバイトとして何らかの処理を行う
}
rope& operator=(CodepointT ch) {
// chをコードポイントとして何らかの処理を行う
}
しかし、これだと、OctetT
でもCodepointT
でもない型の引数を通したい場合、ユーザ側が引数をどちらかにキャストをしないと、曖昧なオーバーロードとしてエラーが出ます。いちいちキャストを書くのは面倒くさいので、何かいい方法はないでしょうか。
template と特化
operator=
を実装してみましょう。
template <typename GeneralT>
rope& operator=(GeneralT ch) {
// chをバイトとして何らかの処理を行う
}
template <>
rope& operator=(CodepointT ch) {
// chをコードポイントとして何らかの処理を行う
}
引数の型がCodepointT
ならば、コードポイント版が呼び出されます。そうでない場合は、バイト版が呼び出されます。template の典型的な用例だと思います。