はじめに
C++で親クラスと複数の子クラスがある、一般的な継承関係において子クラスの比較演算の実装をするのは意外と面倒。最近このパターンを教えることが連続であったため、練習用に備忘しておきます。
セットアップ
まずは前述の継承関係をセットアップします。
今回は、今日、明日、n日後を表現するクラスを作ってみます。
///
/// @brief 今回の日付を表現するクラスのベース
///
class DateExpression {
public:
virtual ~DateExpression() = default;
};
# 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());
}
# 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());
}
# 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));
}
どこに問題があるか?
そして、これらを比較してみたい気持ちになる。
const auto t0 = Today::make_unique();
const auto t1 = After::make_unique(5u);
const bool ret = *t0 < *t1; // ???
しかしこのコードはもちろん動かない。なぜならば、DateExpressionに比較演算子が定義されていないからだ。
では、DateExpressionに比較演算子を定義してみよう。
class DateExpression {
public:
virtual ~DateExpression() = default;
public:
bool operator <(const DateExpression& other) const;
};
bool DateExpression::operator <(const DateExpression& other) const {
//あれ、どうやって実装するんだ??
return false; //とりあえずダミーの実装
}
この比較演算子の実装をどうやるかが悩ましいというのが、今回のテーマ。
順序の実装
戦略1. 数値化しちゃう
これが一番シンプルです。Today
は0
、Tomorrow
は1
でAfter
は中のn
にマップして比較してしまえばよいのです。
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();
}
# 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;
}
# 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;
}
# 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
への変換インタフェースをつけておきます。
class Today;
class Tomorrow;
class After;
class IComparable;
class DateExpression;
# 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;
}
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;
};
# 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());
}
# 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());
}
# 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
側にインターフェースを追加すれば実装し忘れも起きないため、これはこれで悪くない構造かと思います。