Entry

プログラミングメモ - 異なる型を収められる配列を C++ で作る

2009年10月05日

言うまでもないことですけれど,C++ は静的な型付け言語で,ひとつの配列に複数の型のオブジェクトを格納することができません。これを C++ でやる方法はないもんでしょうか。例えば,配列を '(' と ')' で括り(各要素は ' ' で区切る),文字列を '"' で括る場合,次のような具合に格納したい。

( 10 "string" ( "inner" 15 2 "hello" ) 123 )

問題は,配列クラスの作り方で,収め方を工夫することになります。具体的には,配列の要素になるクラスに共通の基底クラス(例えば class Object)を作って,この基底クラスのポインタを配列に収めれば,ひとつの配列(std::vector<Object*>)に収めることができます。

ただ,以前書いたけれども,これでも問題があります。どゆことかというと,配列から値を取り出したとき Object* 型の変数として受け取ることになるので,それが実際にどの型なのか分からないことです。具体的な操作については,RTTI の機構を使って dynamic_cast<> を使うしかないんですけれど,特に問題なのが,コピーコンストラクタや operator= のような複製操作の扱いです。

Object* 型の変数をコピーする場合,そのままコピーすると Object(const Object& other) のような基底クラスのコピーコンストラクタは動くわけですけれど,実際の派生クラスのコピーコンストラクタが動いてくれません。まともにやろうとすると,次のように dynamic_cast<> で無理矢理キャストして,コピーコンストラクタを動かすことになります。

Integer* integer = dynamic_cast<Integer*>(ary.get());  // 配列から取り出してキャスト
Integer newobj(*integer);  // コピーコンストラクタを動かす

実際は,dynamic_cast<> の成否を調べるので,登録した型の数分,if-else が繰り返されることになります。しかし,派生クラスへの dynamic_cast<> の成否で場合分けするのは,遅いし汚いしで,あまりやりたくありません。こゆのも一応外から見たらポリモフィズムの類になるんでしょうけれど,もっと賢いやり方があるわけです。背景が長くなっちゃったけど,そんな感じ。

この話,本当は「仮想関数による多態性の実現」とかいったタイトルにしようかと思ったんですけれど,ちと分かりづらいので,上のようなお題を作ることにしました。こゆ話は,あまり書籍に書いてなくて,例えば『C++ スタイルブック』なんかでも,さらっと書いてあるだけだったりします。

C++ スタイルブック (IT Architects’ Archive―CLASSIC MODERN COMPUTING)
Trevor Misfeldt Gregory Bumgardner Andrew Gray
翔泳社
売り上げランキング: 86577
おすすめ度の平均: 4.0
4 携帯すべし
5 リバースエンジニアリングの観点からも○
4 C++以外の言語にも役に立つ
4 コードをきれいに書くために参考に…
4 一冊持っていて良い

しかも,項目が dynamic_cast<> の話なので,中身を読まないと見つけられないし,しっかり読まないと応用場面を想定することも難しいんじゃないかと思います。

123. dynamic_cast<> を決してポリモーフィズムの代用として使用してはならない

(snip)

上記の例で,Rectangle クラスを追加するのなら,Matrix::rotate() に新たな else-if ブロックを追加する必要がある。オブジェクトの型の適切な振る舞いを選択するには,そうではなく,仮想関数に基づいたポリモーフィズムを使用すべきである。

『C++ スタイルブック 』(Trevor Misfeldt et al.,翔泳社,2006年,pp94-95)

ちと引用だけじゃ分かりにくいと思うので補足しておくと,引用中「上記の例」というのは,dynamic_cast<> の成否(dynamic_cast<> は失敗すると 0 が返る)を使って基底クラスのポインタがどの下位クラスを持っているか決めることです。それはダメ,という話。代わりに,仮想関数で多態性(ポリモフィズム)を実現しましょう,と。

C++ で多態性というと,メソッド引数のオーバーロードしか思いつかない方もいると思うけれども,実際,多態性を実現する方法はたくさんあるし,よく使われています。例えば,テンプレートを使った多態性もしかり。今回の仮想関数を使った方法もそのひとつです。基底クラスを継承したクラスが,仮想関数テーブル上の関数ポインタ(_vfptr)を共有していることを利用しています。

ま,能書きはいいので,とりあず実装例を出しておきます。全部出すと分かりづらいので,クラスの利用側の利用例と,出力結果をまず紹介。こんな風に使います。

int
main(int argc, char* argv[]) {
    try {
        Array ary;
        Array inner;

        // 内側の配列を作る
        inner.add(Integer(12));
        inner.add(String("hello world."));
        inner.add(String("this is string"));
        inner.add(Integer(4));
        // 外側の配列を作る
        ary.add(Integer(2));
        ary.add(inner);
        ary.add(String("outer array"));
        // 文字列で出力する
        std::cout << ary.str() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
    } catch (...) {
        std::cerr << "unknown error" << std::endl;
    }

    return 0;
}

出力はこんな感じになります。

>sample
( 2 ( 12 "hello world." "this is string" 4 ) "outer array" )

配列の中に配列を入れることができます。それでは,すべてのソースです。

#include <iostream>
#include <sstream>
#include <string>
#include <vector>

/**
 * すべての型の基底クラス
 * 配列に収めるにはこのクラスを継承する必要があります。
 */
class Object {
protected:
    Object() throw () {}
    Object(const Object& other) throw () {}
public:
    virtual ~Object() throw () {}
protected:
    Object& operator=(const Object& rhs) throw () {}
public:
    virtual std::string str() const throw () = 0;
    virtual Object* clone() const throw () = 0;
};

/**
 * 配列クラス
 * std::vector<Object*> をラップしたクラスで,複数の型のオブジェクトをまとめる
 * ことができます。文字列出力時は '(' と ')' で括られます。また,各要素は' 'で
 * 区切られます。
 *
 * 配列も当然オブジェクトなので"配列の配列"を作ることができます。
 */
class Array : public Object {
public:
    Array() throw () {}
    Array(const Array& other) throw () {
        typedef std::vector<Object*>::const_iterator Itr;
        ary_.reserve(other.ary_.size());
        for (Itr itr = other.ary_.begin(); itr != other.ary_.end(); ++itr) {
            add(**itr);
        }
    }
    virtual ~Array() throw () {
        typedef std::vector<Object*>::const_iterator Itr;
        for (Itr itr = ary_.begin(); itr != ary_.end(); ++itr) {
            delete *itr;
        }
    }
public:
    Array& operator=(const Array& rhs) throw () {
        if (this != &rhs) {
            std::vector<Object*>::const_iterator itr;
            for (itr = ary_.begin(); itr != ary_.end(); ++itr) {
                delete *itr;
            }
            ary_.empty();
            ary_.reserve(rhs.ary_.size());
            for (itr = rhs.ary_.begin(); itr != rhs.ary_.end(); ++itr) {
                add(**itr);
            }
        }
        return *this;
    }
public:
    void add(const Object& obj) throw () {
        ary_.push_back(obj.clone());
    }
public:
    virtual Object* clone() const throw () {
        return new Array(*this);
    }
    virtual std::string str() const throw () {
        typedef std::vector<Object*>::const_iterator Itr;
        std::stringstream ss;
        ss << "( ";
        for (Itr itr = ary_.begin(); itr != ary_.end(); ++itr) {
            ss << (*itr)->str() << " ";
        }
        ss << ")";
        return ss.str();
    }
private:
    std::vector<Object*> ary_;
};

/**
 * 整数型クラス
 * int 型をラップしたクラスです。文字列出力時は,10進表現の数字を出力します。
 * 実際は計算できる方が便利なので,operator+ 等をオーバーロードして使うことに
 * なります。
 */
class Integer : public Object {
public:
    explicit Integer(int val = 0) throw () : val_(val) {}
    Integer(const Integer& other) throw () : val_(other.val_) {}
    virtual ~Integer() throw () {}
public:
    Integer& operator=(const Integer& rhs) throw () {
        if (this != &rhs) {
            val_ = rhs.val_;
        }
        return *this;
    }
    Integer& operator=(int rhs) throw () {
        val_ = rhs;
        return *this;
    }
public:
    virtual Object* clone() const throw () {
        return new Integer(*this);
    }
    virtual std::string str() const throw () {
        std::stringstream ss;
        ss << val_;
        return ss.str();
    }
private:
    int val_;
};

/**
 * 文字列型クラス
 * std::string をラップしたクラスです。文字列出力時は '"' で括られます。
 */
class String : public Object {
public:
    String(const std::string& str = "") throw () : str_(str) {}
    String(const String& other) throw () : str_(other.str_) {}
    virtual ~String() throw () {}
public:
    String& operator=(const String& rhs) throw () {
        if (this != &rhs) {
            str_ = rhs.str_;
        }
        return *this;
    }
    String& operator=(const std::string& rhs) throw () {
        str_ = rhs;
        return *this;
    }
public:
    virtual Object* clone() const throw () {
        return new String(*this);
    }
    virtual std::string str() const throw () {
        std::stringstream ss;
        ss << "\"" << str_ << "\"";
        return ss.str();
    }
private:
    std::string str_;
};


////////////////////////////////////////////////////////////////////////////////
int
main(int argc, char* argv[]) {
    try {
        Array ary;
        Array inner;

        // 内側の配列を作る
        inner.add(Integer(12));
        inner.add(String("hello world."));
        inner.add(String("this is string"));
        inner.add(Integer(4));
        // 外側の配列を作る
        ary.add(Integer(2));
        ary.add(inner);
        ary.add(String("outer array"));
        // 文字列で出力する
        std::cout << ary.str() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
    } catch (...) {
        std::cerr << "unknown error" << std::endl;
    }

    return 0;
}

どこがポイントか分かるでしょうか。

まず,配列の持ち方については,std::vector<Object*> で持っています。これは先述の通り。

次のポイント。これが多態性のポイントなんですけれど,Object クラスの純粋仮想関数に Object::str() メソッドと Object::clone() メソッドを追加していることです。str() は,その型の文字列表現を std::string 型のオブジェクトにして返すメソッド。clone() は自分自身の複製を返すメソッドです。これらの仮想関数を,各派生クラスで実装しておけば,Object* 型のポインタからでも,_vfptr を通じて派生クラスの各メソッドを利用できることになります。つまり,利用側は Object* 型が具体的にどんな型か知らなくても,str() や clone() を呼ぶだけで,派生クラスで実装した適切な文字列や派生クラスの複製を取得することができるわけです。

つまり,Object::clone() メソッドを各派生クラスで実装することで,実質的に Object 型のポインタから派生クラスのオブジェクト作るコピーコンストラクタを実装したということ。仮想関数による多態性というのは,こういうことです。で,見ていただければ分かる通り,この仕組みは,Array クラスのコピーコンストラクタで,決定的に重要な役割を担っています。

さらに,これは付け足しですけれど,「配列の配列」のようなデータの持ち方を,文字列で表現する場合に,簡易版の Composite パターンを使っています。配列の中に配列がある場合,Array::str() メソッド内の for-文で再帰的に Array::str() メソッドが呼ばれます。呼び元が配列オブジェクトの再帰的な呼び出しに「気づいていない」というのがポイントです。少し注意するところはあるけれど,この方法で,後からいくらでもクラスを追加することができます。

もっとも,もちっと使えるようにするには,もう少し工夫が必要で,文字列オブジェクトのコピーを返しているのはあまりよくありません。std::ios 系のストリームを使えば,もう少し効率的に文字列を生成することができるはずです。

実はこの仕組み,自前でシコシコ作ってる PDF ライブラリの基本機構として利用していたりします。このサイトを読んでいただいている方は,数ヶ月前からうんうん唸っているところを見ていただいていると思うんですけれど,なんとかここら辺まで書けました(上の例は,かなり端折って書き直したもの)。この手の実装は,最初の核になる機構を作るところが大変で,一旦決まればあとはかなり楽になったりします。実際,この方法を使うと,型の追加が本当に楽になるし,型安全もかなり守られます。

ま,JavaScript なんかを使ってれば,いちいち悩むようなことでもないわけで,一瞬で終わる話なんですけどね(空気は読む必要があるが)。低水準はつらいよ,といったところでしょうか。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN