本記事は、C++11 Advent Calendar 2011 : ATNDの2日目の記事です。


C++のclassは様々な使い方ができます。後発のほかの言語ではいくつもの概念に分かれているものも、C++ではすべてclassということもあります。

そこで、C++でclassを定義する際も、classと一括りにせず、自分がいったいどんなclassを書こうとしているのか明確に意識するとよいのではないだろうかと考えました。そのために、私なりのclassの分類をまとめ、この記事を書くことにしました。

これは、各々のプログラミング言語の経験により違いが出ることと思います。異論もあると思いますので、ご自身でも考えてみるとよいと思います。

以下、この記事では4種類に分類しています。


1つ目は「オブジェクト指向プログラミング (OOP) を実現するクラス」です(長いので以下OOPクラスと略します)。

virtual関数を使い、クラスの継承を用いて、多態性を表現するプログラミングスタイルを実現するクラスです。基本的に、これに分類するclass同士の継承は単独継承(すべてpublic)のみです。後述するインタフェースやMix-inのclassを1つまたは複数追加で継承することは構いません。厳密に言うと多重継承は禁止ではありませんが、istream/ostream←iostreamのようにうまいことハマる例は珍しいと思います。

class Widget
{
public:
    int GetId();
    virtual void Draw() = 0;
    virtual Rectangle GetBound();
    virtual ~Widget() = default;
 
private:
    std::unique_ptr<graphic_t> graphic_t;
    std::unique_ptr<window_handle_t> window_handle;
 
    Widget(Widget const&) = delete;
    Widget(Widget&&) = delete;
    Widget& operator =(Widget const&) = delete;
    Widget& operator =(Widget&&) = delete;
};
 
class TextBox : Widget
{
public:
    virtual void Draw() override;
    virtual Rectangle GetBound();
    virtual ~TextBox() = default;
 
private:
    std::u16string text;
};
 
// ……
デストラクタは原則としてdefaultとし、コンパイラの自動生成に任せます。
リソースの解放処理が必要なら、必ずそれをラップした型を作り、それをメンバとして持つようにします。デバッグログを出力するなどといった解放処理とあまり関係ない目的ならデストラクタを明示的に書いてもよいでしょう。なお、これ(デストラクタは原則としてdefaultにすること)は以下すべての分類のclassに当てはまります。
デストラクタはpublicでvirutalにします。
「非virtualでprotected」や「非virutalでpublic」のclassは、たぶんOOPクラスに当てはまりません
コピーコンストラクタ・コピー代入演算子・ムーブコンストラクタ・ムーブ代入演算子をdeleteします
この種のclassではオブジェクトのコピーはどういう意味を持つのか考えられないことが多いです。あと、スライシングが問題になるという現実もあります。なお、従来通りboost::noncopyableから派生させる方法でも構いません。必要なら、Javaなどオブジェクトへのポインタを持ち運ぶ言語を見習い、newしてunique_ptrまたはmake_sharedしてshared_ptrに入れましょう。

最後のコピー禁止にする話の補足です。希に、コピーを作れるようにしたい場合もあるとは思います。その場合は、JavaやC#のようにCloneメンバ関数を「virtualで」定義しましょう。その場合、Cloneメンバ関数の実装を目的として、protectedなコピーコンストラクタを定義しても構いません。

class Node
{
public:
    virtual std::unique_ptr<Node> Clone();
    virtual ~Node() = default;
 
protected:
    Node(Node const&) = default;
 
private:
    std::string name;
 
    Node(Node&&) = delete;
    Node& operator =(Node const&) = delete;
    Node& operator =(Node&&) = delete;
};
 
class TextNode : public Node
{
public:
    virtual std::unique_ptr<Node> Clone() override;
 
protected:
    TextNode(TextNode const&);
};
 
std::unique_ptr<Node> TextNode::Clone()
{
    // C++14からはmake_unique関数が便利です。
    // return std::make_unique<TextNode>(*this);
    return std::unique_ptr<Node>(new TextNode(*this));
}

2つ目、「インタフェース」です。

JavaとかC#とかのinterfaceです。C++だと純粋仮想関数を並べたクラスとして表現します。

class IHogeable
{
public:
    virtual void Foo(int x, int y) = 0;
 
    virtual ~IHogeable() = default;
 
private:
    IHogeable(const IHogeable&) = delete;
    IHogeable(IHogeable&&) = delete;
    IHogeable& operator =(const IHogeable&) = delete;
    IHogeable& operator =(IHogeable&&) = delete;
};

名前にIを付けているのは、私の趣味(COMや.NET由来)なので真似なくて構いません。

インタフェースは、他のインタフェースを複数継承しても構いません。OOPクラスは1つあるいは複数のインタフェースを継承して構いません。インタフェースがOOPクラスあるいは他のclassを継承するのはあり得ません(インタフェースがboost::noncopyableから派生するというよう例外はありますが)。

デストラクタをpublicでvirtualに、コピー・ムーブ禁止であるところはOOPクラスと同じです。

実際のところ、C++で純粋仮想関数ばかりを並べたインタフェースというのはあまり作らないように思います。大抵、NVIパターン (Non-Virtual Interface Idiom)で作ると思うのです(NVIについてはNon-Virtual Interface Idiom – Radium Software Developmentなどを参照してください)。これはこのインタフェースに分類すべきか次のMix-inに分類すべきか、若干悩ましいです。

class IHogeable
{
public:
    void Foo(int x, int y)
    {
        if (x < 0 || y < 0)
        {
            throw new out_of_range("……");
        }
        FooImpl(x, y);
    }
 
    void Foo(Point pt)
    {
        return Foo(pt.X, pt.Y);
    }
 
    virtual ~IHogeable() = default;
 
protected:
    virtual void FooImpl(int x, int y) = 0;
 
private:
    IHogeable(const IHogeable&) = delete;
    IHogeable(IHogeable&&) = delete;
    IHogeable& operator =(const IHogeable&) = delete;
    IHogeable& operator =(IHogeable&&) = delete;
};

3つ目、「Mix-in」です。

制約を課して多重継承を使うという考え方といって合っているでしょうかね。私が最初に見たまともな説明はまつもと直伝 プログラミングのオキテ 第3回(3) – まつもと直伝 プログラミングのオキテ:ITproだったような気がします。ここには、Mix-inについて次のように書いてあります。

Mix-inというのは元々Lisp界で始まった多重継承の使い方です。Mix-in手法には次の2つの条件があります。

  • 通常の継承は単一継承に限る
  • 2つめ以降の継承は,Mix-inと呼ばれる抽象クラスからに限定する

Mix-inクラスは以下のような特徴を備えた抽象クラスです。

  • 単独でインスタンスを作らない
  • 通常のクラスから継承しない

私としては、Mix-in classへアップキャストしての(実行時の)多態性は必要としないという印象があります。そのため、Mix-in classではprotected非virtualデストラクタとします。Mix-in classを継承する際はprotectedまたはprivate継承という場合もあります。また、CRTP (Curiously recurring template pattern)との相性も良いです。

Mix-inのclassは、インタフェース同様1つあるいは複数のMix-inから派生して構いません。また、インタフェースを1つまたは複数継承することも構いません。

コピー・ムーブのコンストラクタ・代入演算子はケースバイケースです。というのも、このMix-inのclass、用途によってはOOPクラスのほか、後述するデータ抽象のclassが継承することもあり得るためです。次の可能性があり得るでしょう。

  • すべてdelete(OOPクラス・インタフェース同様)
  • ムーブのみprotectedで定義
  • ムーブ・コピーをprotectedで定義

いずれにせよ、定義する場合は原則としてdefaultで定義しましょう、という点は変わりません。

Boost.OperatorsやBoost.Noncopyableもここに分類します。

template<typename T>
class IUnknownCounter
{
    long AddRefImpl()
    {
        return count++;
    }
 
    long ReleaseImpl()
    {
        long ret = --count;
        if (ret == 0)
        {
            delete boost::polymorphic_downcast<T>(this);
        }
        return ret;
    }
 
protected:
    ~IUnknownCounter() = default;
    IUnknownCounter(const IUnknownCounter&) = delete;
    IUnknownCounter(const&&) = delete;
    IUnknownCounter& operator =(const IUnknownCounter&) = delete;
    IUnknownCounter& operator =(IUnknownCounter&&) = delete;
 
private:
    std::atomic<long> count;
};

4つ目、本日の最後、「データ抽象」です。

これは、virutalメンバ関数による多態性の実現を利用しないclassです。標準ライブラリでも各種STLコンテナやイテレータ、std::basic_string (std::string)、std::threadなど多数存在します。

デストラクタは非virtualでpublicです。継承は原則として用いませんが、Mix-in classを継承することはあります(例:イテレータの実装にboost::iterator_facade (Mix-in)を継承)。

ムーブコンストラクタ・ムーブ代入演算子はできる限りpublicに定義(もちろんdefault定義を推奨)、可能ならコピーコンストラクタ・コピー代入演算子も定義(もちろんdefault定義を推奨)します。

class UserInfo
{
public:
    UserInfo(std::string userName, ptime loginTime);
 
    std::string GetName() const;
    ptime GetLastLoginTime() const;
 
    UserInfo(UserInfo const&) = default;
    UserInfo(UserInfo&&) = default;
    UserInfo& operator =(UserInfo const&) = default;
    UserInfo& operator =(UserInfo&&) = default;
 
    ~UserInfo() = default;
 
private:
    std::string name;
    ptime lastLogin;
};

繰り返しますが、どの場合でもデストラクタはdefaultで定義します。自分自身では書きません。どういうことかと言うと、必要な解放処理はできる限りメンバ変数(のデストラクタ)に任せることで、自分(このクラス)自身では「特にデストラクタでやることはない」という状態を目指します。その解放処理のためのクラスだけが唯一デストラクタを明示的に定義してよい例外パターンです。

なお、それすらもstd::shared_ptrやstd::unique_ptrで足りる場合も多いでしょう。unique_ptrについては以下が参考になると思います。

最後に、今日の記事の大元のネタはC++ クラス設計に関するノートです。これを自分なりに解釈して考えをまとめたいという思いで、本記事を書こうと思いました。感謝いたします。


  • 2012年5月8日:誤字の修正や表現の変更を行いました。
  • 2015年1月18日:例示のコードの誤りの修正と表現の変更(主に漢字・かなの選択について)を行いました。
スポンサード リンク

この記事のカテゴリ

  • ⇒ C++11時代におけるクラスの書き方