3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ダウンキャストせずに子オブジェクトを比較する

Last updated at Posted at 2018-05-22

はじめに

C++で親クラスと複数の子クラスがある、一般的な継承関係において子クラスの比較演算の実装をするのは意外と面倒。最近このパターンを教えることが連続であったため、練習用に備忘しておきます。

セットアップ

まずは前述の継承関係をセットアップします。
今回は、今日、明日、n日後を表現するクラスを作ってみます。

DateExpression.hpp
///
/// @brief 今回の日付を表現するクラスのベース
///
class DateExpression {
public:
    virtual ~DateExpression() = default;
};
Today.hpp
# include <memory>    //unique_ptr

class Today : public DateExpression {
public:
    static std::unique_ptr<const DateExpression> make_unique();

private:
    Today() = default;
};

std::unique_ptr<const DateExpression> Today::make_unique()
{
    return std::unique_ptr<const DateExpression>(new Today());
}
Tomorrow.hpp
# include <memory>    //unique_ptr

class Tomorrow : public DateExpression {
public:
    static std::unique_ptr<const DateExpression> make_unique();

private:
    Tomorrow() = default;
};

std::unique_ptr<const DateExpression> Tomorrow::make_unique()
{
    return std::unique_ptr<const DateExpression>(new Tomorrow());
}
After.hpp
# include <cstddef>    //size_t
# include <memory>    //unique_ptr

class After : public DateExpression {
public:
    static std::unique_ptr<const DateExpression> make_unique(const std::size_t n);

private:
    After(const std::size_t n) : _n(n) { assert(n >= 2); }

private:
    std::size_t _n;
};

std::unique_ptr<const DateExpression> After::make_unique(const std::size_t n)
{
    return std::unique_ptr<const DateExpression>(new After(n));
}

どこに問題があるか?

そして、これらを比較してみたい気持ちになる。

compare.cpp
const auto t0 = Today::make_unique();
const auto t1 = After::make_unique(5u);

const bool ret = *t0 < *t1; // ???

しかしこのコードはもちろん動かない。なぜならば、DateExpressionに比較演算子が定義されていないからだ。
では、DateExpressionに比較演算子を定義してみよう。

DateExpression.hpp
class DateExpression {
public:
    virtual ~DateExpression() = default;

public:
    bool operator <(const DateExpression& other) const;
};

bool DateExpression::operator <(const DateExpression& other) const {
    //あれ、どうやって実装するんだ??
    return false; //とりあえずダミーの実装
}

この比較演算子の実装をどうやるかが悩ましいというのが、今回のテーマ。

順序の実装

戦略1. 数値化しちゃう

これが一番シンプルです。Today0Tomorrow1Afterは中のnにマップして比較してしまえばよいのです。

DateExpression.hpp
class DateExpression {
public:
    virtual ~DateExpression() = default;

public:
    bool operator <(const DateExpression& other) const;

private:
    virtual std::size_t to_number() const = 0;
};

bool DateExpression::operator <(const DateExpression& other) const {
    return this->to_number() < other.to_number();
}
Today.hpp
# include <memory>    //unique_ptr

class Today : public DateExpression {
public:
    static std::unique_ptr<const DateExpression> make_unique();

private:
    Today() = default;
    virtual std::size_t to_number() const override;
};

std::unique_ptr<const DateExpression> Today::make_unique()
{
    return std::unique_ptr<const DateExpression>(new Today());
}

std::size_t Today::to_number() const
{
    return 0;
}
Tomorrow.hpp
# include <memory>

class Tomorrow : public DateExpression {
public:
    static std::unique_ptr<const DateExpression> make_unique();

private:
    Tomorrow() = default;
    virtual std::size_t to_number() const override;
};

std::unique_ptr<const DateExpression> Tomorrow::make_unique()
{
    return std::unique_ptr<const DateExpression>(new Tomorrow());
}

std::size_t Tomorrow::to_number() const
{
    return 1;
}
After.hpp
# include <cstddef>
# include <memory>

class After : public DateExpression {
public:
    static std::unique_ptr<const DateExpression> make_unique(const std::size_t n);

private:
    After(const std::size_t n) : _n(n) { assert(n >= 2); }
    virtual std::size_t to_number() const override;

private:
    std::size_t _n;
};

std::unique_ptr<const DateExpression> After::make_unique(const std::size_t n)
{
    return std::unique_ptr<const DateExpression>(new After(n));
}

std::size_t After::to_number() const
{
    return _n;
}

戦略2. ダブルディスパッチ

間にIComparableを作って子クラスたちに継承させ、Visitorパターンで結果を返すやり方です。
DateExpressionにはIComparableへの変換インタフェースをつけておきます。

fwd.h
class Today;
class Tomorrow;
class After;
class IComparable;
class DateExpression;
IComparable.hpp
# include "fwd.h"

class IComparable {
public:
    virtual ~IComparable() = default;

public:
    bool greater(const Today& other) const { return this->greaterImpl(other); }
    bool greater(const Tomorrow& other) const { return this->greaterImpl(other); }
    bool greater(const After& other) const { return this->greaterImpl(other); }

private:
    virtual bool greaterImpl(const Today& other) const = 0;
    virtual bool greaterImpl(const Tomorrow& other) const = 0;
    virtual bool greaterImpl(const After& other) const = 0;    
}
DateExpression.hpp
class DateExpression {
public:
    virtual ~DateExpression() = default;

    bool operator <(const DateExpression& other) const
    {
        return this->less(other.to_comparable());
    }

private:
    virtual bool less(const IComparable& other) const = 0;
    virtual const IComparable& to_comparable() const = 0;
};
Today.hpp
# include <memory>

class Today : public DateExpression, public IComparable {
public:
    static std::unique_ptr<const DateExpression> make_unique();

private:
    Today() = default;

private:
    virtual bool less(const IComparable& other) const override 
    {
        return other.greater(*this);
    }

    virtual const IComparable& to_comparable() const override 
    {
        return static_cast<const IComparable&>(*this);
    }

    virtual bool greaterImpl(const Today& other) const override { return false; }
    virtual bool greaterImpl(const Tomorrow& other) const override { return false; }
    virtual bool greaterImpl(const After& other) const override { return false; }
};

std::unique_ptr<const DateExpression> Today::make_unique()
{
    return std::unique_ptr<const DateExpression>(new Today());
}
Tomorrow.hpp
# include <memory>

class Tomorrow : public DateExpression, public IComparable {
public:
    static std::unique_ptr<const DateExpression> make_unique();

private:
    Tomorrow() = default;

private:
    virtual bool less(const IComparable& other) const override 
    {
        return other.greater(*this);
    }

    virtual const IComparable& to_comparable() const override 
    {
        return static_cast<const IComparable&>(*this);
    }

    virtual bool greaterImpl(const Today& other) const override { return true; }
    virtual bool greaterImpl(const Tomorrow& other) const override { return false; }
    virtual bool greaterImpl(const After& other) const override { return false; }
};

std::unique_ptr<const DateExpression> Tomorrow::make_unique()
{
    return std::unique_ptr<const DateExpression>(new Tomorrow());
}
After.hpp
# include <cstddef>
# include <memory>

class After : public DateExpression, public IComparable {
public:
    static std::unique_ptr<const DateExpression> make_unique(const std::size_t n);

private:
    After(const std::size_t n) : _n(n) { assert(n >= 2); }

private:
    virtual bool less(const IComparable& other) const override
    {
        return other.greater(*this);
    }

    virtual const IComparable& to_comparable() const override 
    {
        return static_cast<const IComparable&>(*this);
    }

    virtual bool greaterImpl(const Today& other) const override { return false; }
    virtual bool greaterImpl(const Tomorrow& other) const override { return false; }
    virtual bool greaterImpl(const After& other) const override { return this->_n > other._n; }

private:
    std::size_t _n;
};

std::unique_ptr<const DateExpression> After::make_unique(const std::size_t n)
{
    return std::unique_ptr<const DateExpression>(new After(n));
}

で、どちらがいいのか?

戦略1のメリットは何と言っても、構成のシンプルさだと思います。今回のケースのように数値が自然に定義でき、あとから間に順序が入らないことが明白なケースでは絶対に戦略1のほうがよいです。
そして戦略2ですが、こちらはなんといっても複雑さがデメリットですね。ただ、こちらも一度書いてしまえば、クラスの追加はほぼコピペで済みますし、IComparable側にインターフェースを追加すれば実装し忘れも起きないため、これはこれで悪くない構造かと思います。

3
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?