Entry

プログラミングメモ - 汎用配列とダウンキャストの話

2009年01月16日

PDF の一部ライブラリでも作ろうかと思って,仕様書を読んでるんですけど,設計的に極悪というか,「どーしよこれ」という状態になってます。多分,他の場面でも問題になると思うんですが。

困っているのが,配列(とか辞書とか)の話。PDF には配列型という型があるんですけれど,これ,どんな型でも詰め込めちゃう。擬似言語っぽく書くと,例えば,

Array array = {23, "String", true, null, {12, false}};

みたいなことができちゃう。JavaScript みたいな型のない言語(というと語弊があるが)で書くと簡単なんですけれど,C++ みたいなガチガチの型付き言語だと,かなりつらい。

で,大体みんな思いつくのが,こんな感じで,抽象クラスにまとめちゃうことだと思うんです。

class Object {
public:
    Object();
    virtual ~Object();
public:
    // いろいろ
};

class String : public Object {
public:
    String();
    virtual ~String();
public:
    // さまざま
};

class Numeric : public Object {
public:
    Numeric();
    virtual ~Numeric();
public:
    // それぞれ
};

class Array {
public:
    Array();
    virtual ~Array();
public:
    void set(const Object& obj);
    Object& get() const;
private:
    std::vector<Object*> array_;
};

これで,Array クラスに String クラスも Numeric クラスも格納することができます。ただ,問題はアクセサです。各要素にアクセスする場合,Array オブジェクトからは,当然 Object 型のポインタ/参照としてもらうわけで,それが String オブジェクトなのか Numeric オブジェクトなのかは分からない。けど,もらったオブジェクトを使うには,こいつをどれかにキャストしないといけないわけですね。

一応,PDF の仕様書からは,どの要素に何が入るのか大体分かるんですけれど,それじゃ低レベルの型として配列を定義した意味がない。また,何よりも,C++ の(一部)お作法として,「ダウンキャストするやつは死ね!」(言いすぎ)というのがあるわけで,おいそれとダウンキャストはできないわけです。個人的にも RTTI 的なことはあまりやりたくない。

で,これ,ちゃんとやるとすると,親のクラス(Object)に子供のオブジェクトのアクセサを全て持たせておいて,子供のオブジェクトでそれぞれ適当なアクセサを定義するのが正解なんだと思います。

例えば,Boolean なオブジェクトを Boolean で受けたら,当然 true/false が返るように定義するわけですけれど,String で受けたら,"true"/"false"の文字列を返すようにしたり,Numeric で受けたら 1/0 を返したりってなように実装する……と。ぽりもふぃずむってやつですか。そゆやつ使うわけです。もちろん,どこかひとつで必要な演算は,多少無理目でも,他の子供に全て定義する。Boolean + Boolean とかってどうなるんだろう(論理和か?)……とかウジウジ考えない。

もっともこの話,ちょっと前にも触れたんですけれど(参照:qune: Command パターンを Web アプリで使う意義),こゆことをするためには,アクセサを抽象化できるだけの共通性が必要だったりする。ここで Object 型にまとめたいのは,単にコンテナに詰め込みやすくするだけ,といった実装主導の都合なので,設計的にはあまりよくないのかもしれません。

そもそも,PDF の仕様書によると,配列もオブジェクトなので,配列も配列の中に組み込めることになっている(多次元配列はサポートされていない)。しかし,インデックスアクセスとかイテレータの扱いとかいったような配列に特有の処理を,他のリテラルなオブジェクト(なんか変な表現だけど Boolean とか String のようなオブジェクトのこと)に共通して持たせるってのは,無理ってもんだ。

と,そんなこんなで,どうにもならないので,先行してる人がどんな風に作っているのか見てみることにしました。tetex のソースなんですけど,Object クラスをこんな風に定義してあった(体裁を調整しています)。

enum ObjType {
  // simple objects
  objBool,                      // boolean
  objInt,                       // integer
  objReal,                      // real
  objString,                    // string
  objName,                      // name
  objNull,                      // null
  // complex objects
  objArray,                     // array
  objDict,                      // dictionary
  objStream,                    // stream
  objRef,                       // indirect reference
  // special objects
  objCmd,                       // command name
  objError,                     // error return from Lexer
  objEOF,                       // end of file return from Lexer
  objNone                       // uninitialized object
};

class Object {
public:
    // Default constructor.
    Object() : type(objNone) {}

    // (snip)

    // Type checking.
    ObjType getType() { return type; }
    GBool isBool() { return type == objBool; }
    GBool isInt() { return type == objInt; }
    GBool isReal() { return type == objReal; }
    GBool isNum() { return type == objInt || type == objReal; }
    GBool isString() { return type == objString; }
    GBool isName() { return type == objName; }
    GBool isNull() { return type == objNull; }
    GBool isArray() { return type == objArray; }
    GBool isDict() { return type == objDict; }
    GBool isStream() { return type == objStream; }
    GBool isRef() { return type == objRef; }
    GBool isCmd() { return type == objCmd; }
    GBool isError() { return type == objError; }
    GBool isEOF() { return type == objEOF; }
    GBool isNone() { return type == objNone; }

    // Special type checking.
    GBool isName(char *nameA)
        { return type == objName && !strcmp(name, nameA); }
    GBool isDict(char *dictType);
    GBool isStream(char *dictType);
    GBool isCmd(char *cmdA)
        { return type == objCmd && !strcmp(cmd, cmdA); }

    // Accessors.  NB: these assume object is of correct type.
    // ... (オブジェクトタイプごとのアクセサが続く)

    // (snip)

private:
    ObjType type;         // object type
    union {               // value for each type:
        GBool booln;      //   boolean
        int intg;         //   integer
        double real;      //   real
        GString *string;  //   string
        char *name;       //   name
        Array *array;     //   array
        Dict *dict;       //   dictionary
        Stream *stream;   //   stream
        Ref ref;          //   indirect reference
        char *cmd;        //   command
    };
};

えー……union かぁ……。継承すら使ってない。ObjType を内部で持って is*() 関数で型を判別するようです。でも,これって,RTTI を自前で実装してるだけだったりする。型をクラスでかぶせて dynamic_cast の戻り値を見た方が,今時はスマートなのかも。それにしても union かぁ……。一部のスクリプト言語では,たしかこんな実装があった覚えがあるけれども,あれは C だったからそうしてたんだろうな。

つことで(どういうことなんだか分からんけど),結論から言うと,おとなしく dynamic_cast した方が良さそうな気がする。下手なことやってるとオーバーヘッド大きそうだし。でもなんだかゴニョゴニョだなぁ。

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