template library を作る - 本題の前に

since: 2001-12-?? update: 2001-12-?? count:

template という機能はまだ使用例が少ないため確立した技法が 少ないのですが、いくつか注意点をあげておきます。

命名規則

他のプログラム同様、これでなければいけない、というものはありませんが、 一定の命名規則を定めてからコーディングをした方が良いと思います。 私が一番気になるのは、よくサンプルなんかにある

template <class T>
class Hoge {
    // ...
};

なんて奴です。これは本当に class T が不定なら仕方がないのですが、 (例えば std::vector など) 大抵の場合入力反復子しか受け付けない、 など制約条件があることが多いです。 だから

// transform

template <class _InputIter, class _OutputIter, class _UnaryOperation>
_OutputIter transform(_InputIter __first, _InputIter __last,
                      _OutputIter __result, _UnaryOperation __oper) {
  for ( ; __first != __last; ++__first, ++__result)
    *__result = __oper(*__first);
  return __result;
}

のようにテンプレート引数に取りうるクラスの内容を 記述することは非常に重要です。 template 引数はどんなクラスでも許容するので、 何を取りうるのか、という情報はきっちりと記述しないと、 使用者が混乱してしまいます。 できればコメントなどで使用法や制約条件を明記した方が良いでしょう。

ちなみに先ほどの例は SGI の STL なのですが、 この STL は非常に可読性が高く、 template の教材として、かなり良いものであると思います。

この例にはもう一つ学ぶべき点があります。 それはテンプレート引数の命名です。 たぶん、クラス名は大文字で書きはじめるというような規則を 持っている人はかなり多いと思います。 また、プライベートなメンバはアンダーバーで書きはじめる、 という方もかなり多いと思います。 この例では、その二つを合わせたテンプレート引数の命名規則を使っています。 つまり、アンダーバーで書きはじめ、アンダーバー以降の最初の文字は 大文字、というものです。 これはテンプレート引数が内部クラスであることから、 理にかなった命名法であると思います。 他の命名規則と同じく、これが絶対に正しい、というものはありませんが、 自分なりに一貫した、他の文法要素と区別のつくルールを考える必要があります。

typedef

template においては typedef は非常に重要な機能です。 例えば std::list のラッパを作るとして次のどちらがわかりやすいでしょうか?

class MyList {
    std::list<int> _impl;
public:
    std::list<int>::iterator begin() { return _impl.begin(); }
    std::list<int>::iterator end() { return _impl.end(); }

    // ...
}

class MyList {
    typedef std::list<int> _ContainerType;
    _ContainerType _impl;
public:
    typedef _ContainerType::iterator iterator;
    iterator begin() { return _impl.begin(); }
    iterator end() { return _impl.end(); }

    // ...
}

これだけ見てもずいぶん後者の方がわかりやすいと思うのですが、 このクラスを使ってみると違いが明らかになります。

MyList myList;
// iterator の型を知っていなければならない!
std::list<int>::iterator ite;
for (ite = myList.begin(); ite != myList.end(); ite++) {
    // ...
}

MyList myList;
MyList::iterator ite;
for (ite = myList.begin(); ite != myList.end(); ite++) {
    // ...
}

では使用感が天と地ほどの差があるでしょう。 基本的に使用者側にとっては、 MyList が std::list によって作られていることなどどうでも良いのです。 また、こういった抽象化は後々の変更を容易にします。 例えば MyList の実装方法を std::list から std::vector に変更したいとき、 前者でがクラス内の全ての std::list<int> を std::vector<int>に置換して、 ドキュメントを記述して、 既にこのクラスの使用者がいれば平謝りで置換してもらう、 というようなことをしなければなりません。 それに対して、後者ではただ

typedef std::list<int> _ContainerType;

typedef std::vector<int> _ContainerType;

に変更するだけで良いのです。 これは多少極端な例ですが typedef の 重要性の雰囲気は理解して頂けると思います。

標準に準拠する

クラス内の public な情報はできる限り標準に合わせておいた方が良いです。 さきほどの例でも

typedef std::list<int> _ContainerType;

として STL コンパチにしておきましたが、 例えばこれによって、

template <class _Container>
void algorithm(const _Container& cont) {
    _Container::iterator ite;
    for (ite = cont.begin(); ite != cont.end(); ite++) {
        // ...
    }
}

というような STL コンテナのために作ったアルゴリズムが そのまま MyList にも適用できるのです。 (もっともこの場合は普通 iterator をとりますが)

他にも、よくある例としては、関数オブジェクトは必ず std::unary_function か std::binary_function を継承する、 といものがあります。 これは std::*_function の情報を使うアルゴリズム・アダプタがあるからです。 (例えば STL では std::bind1st)

長くなりましたが準備はこんなもんで Sample の紹介を始めていきます。


home / index

全てリンクフリーです。 コード片は自由に使用していただいて構いません。 その他のものはGPL扱いであればあらゆる使用に関して文句は言いません。 なにかあれば下記メールアドレスへ。

shinichiro.hamaji _at_ gmail.com / shinichiro.h