SSブログ

10進数と16進数の整数クラス(設計編) [日記]

文字列で表現された整数を表すクラスがありました。

必要な部分だけ書くと、こんな↓クラスです。

class Value
{
  public:
    Value(const char* value_str);

    // (コンストラクタで)指定した文字列が数値を表しているかどうか。
    bool is_valid() const;

    // 数値を取得する。
    int get_int() const;
};

こんな風に使います。

int main()
{
    char buff[100];
    std::cin >> buff;

    Value value(buff);

    if (value.is_valid()) {
        std::cout << value.get_int() * 2 << "\n";
    } else {
        std::cout << "数値ではない\n";
    }
    
    return 0;
}

イメージとしては、Javaのjava.lang.Integerクラスの様なものです。
java.lang.Integerは整数を表すクラスと言うよりint型のラッパの毛色が強いですが…)


さて、上のクラスは10進数で書かれている整数が対象ですが、
"0x"で始まる場合は16進数と解釈するクラスが必要になりました。

クラス図

そこで、元のクラスを継承して新しいクラスを作ることにしました。
(元のクラスは10進数の整数ということでDecValueという名前にしました)

// 10進数で書かれた整数
class DecValue
{
};

// "0x"で始まれば16進、そうでなければ10進数で書かれた整数
class DecHexValue : public class DecValue
{
};

ですが、
なんとなく違和感が……。

継承の基本は is-a 関係です。

クラスAを派生してクラスBを作った場合、
"B is a A". 「BはAである」
と言えなければなりません。

上の例に当てはめると、
「『16進数もしくは10進数』は『10進数』である」
となります。

いやいやいや。
それ、違うから。

日本語的に正しい言い方に変えるのならば、
「『10進数』は『16進数もしくは10進数』である」
です。

クラス図

ならば、クラスの継承関係も逆でなければなりません。

// 10進数と16進数
class DecHexValue { };

// 10進数
class DecValue : public DecHexValue { };

これはこれで、やはり違和感が……。

既存クラスを拡張する目的で、既存クラスを派生するのではなく、基底クラスを作ってしまうのは、色々と違う気がするのです。
既存のクラス(この場合DecValueを修正できない場合は、そもそも基底クラスを作ることが不可能ですし。

それに、更に似たようなクラスが増えたらどうするのかという問題もあります。

10進数と16進数と8進数に対応したクラス(DecHexOctValue)なら、DecHecValueの基底クラスにすれば良いですが、10進数と2進数のクラス(DecBinValue)は、どうしましょう?


そこで、継承について改めて考えてみました。

継承には、いくつかの種類があります。
is-a関係の継承は、その1つです。

他には、機能の拡張としての継承もあります。

クラス図

ならば、DecValueを継承してDecHexValueを作る(最初の)方法でも別段間違いではないと言うことです。

// 10進数
class DecValue { };

// 10進数と16進数
class DecHexValue : public class DecValue { };
クラス図

10進数と16進数と8進数のクラスや10進数と2進数のクラスとかに備えるなら、10進数だけ、16進数だけのクラスを作っておいて、多重継承にするのが良さそうです。

// 10進数
class DecValue : public virtual  AbstractValue { };

// 16進数
class HexValue : public virtual AbstractValue { };

// 10進数と16進数
class DecHexValue : public DecValue, public HexValue { };

これでバッチリです。


クラスの継承関係だけで考えていたら、これでバッチリだと思っていたのです。

でも、メソッド(メンバ関数)の実装を考えたら、意外と面倒でした。

bool DecHexValue::is_valid() const
{
    return DecValue::is_valid() || HexValue::is_valid();
}

int DecHexValue::get_int() const
{
    if (DecValue::is_valid())
        return DecValue::get_int();

    if (HexValue::is_valid())
        return HexValue::get_int();

    throw std::runtime_error("数値じゃない");
}

こんな感じで、進数(基数)の組み合わせ違いのクラスを作る度に、内部の実装を行わなければならないのです。しかも扱う基数の数が増えれば増えるほど実装も増えていきます。

そこで再度改めて継承について考えて見ます。

「機能の拡張」としての継承は、基底クラスのインスタンスをメンバに持つことでも実現することが出来ます。

そこで、10進数と16進数の整数クラスは、10進数クラスと16進数クラスのインスタンスをメンバ(配列)に入れてループを回すようにすれば、扱う基数の数が増えても実装に変化はありません。

クラス図
class MultiRadixValue : public AbstractValue
{
  private:
    std::vector<AbstractValue*> values_;

    // (略)

  public:
    // 対象とする整数クラスを追加する。
    void addTargetValue(AbstractValue* value);
};

int MultiRadixValue::get_int() const
{
    for (size_t i = 0; i < values_.size(); i++) {
        if (values_[i]->is_valid())
            return values_[i]->get_int();
    }

    throw std::runtime_error("数値じゃない");
};

これで、もう大丈夫かと思っていたのですが、このクラスを使うことを考えると、また面倒なことになっていました。

int main()
{
    char buff[100];
    std::cin >> buff;

    MultiRadixValue value(buff);
    value.addTargetValue(new DecValue(buff));
    value.addTargetValue(new HexValue(buff));

    if (value.is_valid()) {
        std::cout << value.get_int() * 2 << "\n";
    } else {
        std::cout << "数値ではない\n";
    }
    
    return 0;
}

見ての通り。
addTargetValueに渡す整数クラスそれぞれに、同じ整数の文字列を渡さなければならないのです。
指定する文字列を間違えたら、おかしな挙動をしてしまいます。
バグを生む余地を残すようでは、安全なクラスとは言えません。

    MultiRadixValue value(buff);
    value.addTargetValue(new DecValue());
    value.addTargetValue(new HexValue());

...

void MultiRadixValue::addTargetValue(AbstractValue* value)
{
    value->set_value_str(value_str_);
    values.push_back(value);
}

addTargetValueの処理内部で、対象の整数クラスに整数の文字列を設定するようにすれば、文字列の指定を間違える余地は無くなりますが、今度は各整数クラスにデフォルトコンストラクタが必要になります。

デフォルトコンストラクタを用意すると、今度は、デフォルトコンストラクタで作成したインスタンス(不定値)の扱いを考える必要があります。
値は常に初期化された状態で用意し、不定な状態にしない、させない…が安全なプログラミングの原則なので、望ましい状態ではありません。

後、addTargetValueの引数がポインタなので、ポインタの解放忘れや2重解放、スタック上のインスタンスのポインタを渡してしまう等々、面倒事も増えます。


なんだか、何をやっても何かしらの不満点が出てきます。
八方塞がり?

………
………

が、見つけました。
上で挙げた問題点を全て解決する方法。

とりあえず、実装部分は次回は後回しで、使い方だけ紹介。

typedef MultiRadixValue<DecValue, HexValue> DecHexValue;

int main()
{
    char buff[100];
    std::cin >> buff;

    DecHexValue value(buff);

    if (value.is_valid()) {
        std::cout << value.get_int() * 2 << "\n";
    } else {
        std::cout << "数値ではない\n";
    }
    
    return 0;
}

typedef MultiRadixValue<DecValue, HexValue, OctValue> DecHexOctValue;

この件について調べているうちに知ったこと。

  • strtolがC言語の標準関数だったこと
  • strtolの基数に16を指定した時、"0x"が付いていても大丈夫だったこと
[新幹線] 今日の一冊
へんな毒すごい毒 (知りたい★サイエンス)

へんな毒すごい毒

  • 作者: 田中 真知
  • 出版社/メーカー: 技術評論社
  • 発売日: 2006/08/31
  • メディア: 単行本(ソフトカバー)

タグ:C++
nice!(0)  コメント(0)  トラックバック(0) 

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。