メンバ変数を隠蔽する理由

  • 28
    いいね
  • 9
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

数値計算でのオブジェクト指向のコードで、クラスのメンバ変数のアクセス属性をpublicにしているコードをたまに見かけます。特に強制ではないのですが、一般的にメンバ変数はprivateにします。つまり、そのオブジェクトのメンバ変数は外部から隠蔽されます。これはC++に限ったことではなく、JAVAなどのオブジェクト指向言語ではメンバ変数はprivateで隠蔽します。
C++入門書の中には、メンバ変数を隠蔽する明確な理由は書かれない事も多く、慣例的な扱いをされます。そのため今回は次の3つの観点からメンバ変数の隠蔽理由を考えていきます。

publicとprivateのメンバ変数のクラス

2次元のベクトルを例にアクセス属性publicprivate、それぞれのメンバ変数でのクラスを設計してみます。
まず、publicメンバ変数でのクラスは次のとおりです。

public
class vector {
public:
    double x;
    double y;
public:
    vector();
    vector(const vector& other);
    ~vector();

    vector& operator=(const vector& v);
};

publicメンバ変数は外部から直接参照可能なため、処理が重複しないようにメンバ関数は少ないです。このpublicメンバ変数のクラスは、C言語やFortranの構造体とプログラム内ではほぼ同じ処理をするので、

\mbox{publicメンバ変数のクラス} = \mbox{構造体} 

という認識でも大丈夫です。
次にprivateメンバ変数のクラスは次のようになります。

private
class vector {
public:
    vector();
    vector(const vector& other);
    ~vector();

    const double& x() const {
        return this->x_;
    }

    double& x() {
        return this->x_;
    }

    const double& y() const {
        return this->y_;
    }

    double& y() {
        return this->y_;
    }

   vector& operator=(const vector& v);

private:
    double x_;
    double y_;
};

privateメンバ変数をx_という風にアンダーバー_を付けるのは、その変数がprivateであることを明示的にするためです。これは人によって命名方法は違うので、あまり気にしないで下さい。
さてprivateのメンバ変数は外部から直接扱うことができませんので、次のようにメンバ関数にてメンバ変数へアクセスする方法を提供します。

アクセッサ
const double& x() const;
double& x();

// yについても同様

このアクセスするためのメンバ関数をアクセッサ(accessor)と一般的に呼ばれています。さらに個別にconst double& x() constゲッター(getter)double& x()セッター(setter)と呼びます。

パフォーマンス

パフォーマンスを考えるには、アクセス時間を計測してみるのが良いでしょう。アクセスのうち次のように設定する方のみでpublicメンバ変数、privateメンバ変数のパフォーマンスを比較していきます。

public
vector v;
v.x = x;
v.y = y;
private
vector v;
v.x() = x;
v.y() = y;

上記コードでは実行時間が短過ぎて時間計測できないので、10000回繰り返してみました。

public private
1.5E-5 [sec] 4.0E-5 [sec]

結果(10000回繰り返し)は、publicメンバ変数の方が高速です。この原因は単純で、publicメンバ変数は直接参照しているのに対して、privateメンバ変数はセッター(メンバ関数)を介して処理しているので、関数呼び出しがオーバーヘッドとなるからです。もしprivateメンバ変数のセッターがインライン化されていれば、publicメンバ変数と同等の処理速度になります。パフォーマンスだけをみれば、publicメンバ変数が優位ですが、次の整合性保守性を考えるとその優位は微かなものとなります。

整合性

整合とは矛盾がないということです。これを説明するのに丁度良いものがあります。それは幾何学の方向(direction)です。方向は大きさ1の単位ベクトルのことです。方向の定義は大きさ(ノルム)が常に1のベクトルです。大きさ1という制約(定義)は、外部からどんな操作を受けても変化してはいけません。そのため大きさ1が保たれる操作のみを可能とするようなクラス設計を行う必要があります。すべてのメンバ関数は載せませんが、privateメンバ変数を実装した方向のクラスは次のようなものになります。

direction
class direction {
public:
    direction();
    direction(const direction& u);
    explicit direction(const vector& v);
    ~direction();

    const double& x() const;
    const double& y() const;

    void rotate(double angle); // angle分回転させるメンバ関数

    direction& operator=(const direction& u);

   /*
    * 定義の整合性が保たれるその他のメンバ関数
    */

private:
    double x_;
    double y_;
};

ここで重要なのは、メンバ変数へのアクセッサがゲッターのみでセッターを実装していないところにあります。もしセッターを実装してしまったら、方向の定義である大きさ1が保持されず、整合性が破壊されてしまいます。

class direction {
public:
    double& x();
    double& y();

    /*
     * その他のメンバ関数 & メンバ変数
     */
};

direction d;
d.x() = dx; // directionの整合性が破壊される。
d.y() = dy; // 同上

上記の整合性を破壊するような事象は、publicメンバ変数でも起こります。

public
class direction {
public:
    double x;
    double y;

public:
    direction();
    direction(const direction&);
    ~direction();

    direction& operator=(const direction& t);
};

direction d;
d.x = dx; // directionの整合性が破壊される。
d.y = dy; // 同上

クラス(型)の整合性は、それの定義や制約と結びつきます。整合性の破壊は、クラス自体の存在意義を失くします。そのためprivateメンバ変数にて、外部から隠蔽しそのクラスの整合性を保っています。この整合性がメンバ変数を隠蔽する大きな理由となります。

保守性

開発を進めていくとバグの改修やパフォーマンスの改善などで保守が行われます。例えば、ベクトルvectorの例では、$x$座標、$y$座標のそれぞれをdouble型で実装しましたが、パフォーマンスの観点からメンバ変数はdouble型配列にした方が良いです。ここでは詳細は省きますが、コンパイラによる最適化の際にベクトル化される可能性があるからです。このパフォーマンスの理由からメンバ変数をdouble型からdouble型の配列に変更した場合、コード修正が必要になります。
クラス内でのコード修正は、最初で示したpublicとprivateのメンバ変数のクラスのとおり、privateメンバ変数で実装したクラスの方がメンバ関数が多い分、労力がかかるでしょう。例えばアクセッサなどのメンバ変数を参照しているメンバ関数を修正しなければなりません。

メンバ変数変更によるアクセッサ(セッター)の修正
double& vector::x() {
    // return this->x_; // 修正前
    return this->x_[0]; // 修正後
}

しかし、publicメンバ変数ではクラスの外部にて直接参照されているため、すべての箇所でコード修正が必要になります。

public
vector v;
// v.x = x;
// v.y = y;
// コードを書き直す必要がある。
v.x[0] = x;
v.x[1] = y;

一方、privateメンバ変数では、アクセッサの関数名が変わるわけではないのでコード修正は不要です。

private
vector v;
// コードを書き直す必要はない。
v.x() = x;
v.y() = y;

メンバ変数の変更によるコード修正は、privateメンバ変数のときはクラス内だけで局所的、publicメンバ変数では散逸的になります。修正をする際はやはり前者の局所的になっている方が簡単ですし、散逸的に修正が必要では探すのにも手間だったりしますし、参照箇所が多いとかなり大きな作業となるでしょう。この保守性の場合も、privateメンバ変数の方が利点が多いと思います。

まとめ

今回、メンバ変数のアクセス属性において、パフォーマンスの点ではpublicメンバ変数、整合性、保守性の面ではprivateメンバ変数が優位でした。それぞれのメンバ変数でパフォーマンスの優位よりも整合性と保守性での優位の方がとても大きく

\mbox{パフォーマンス} << (\mbox{整合性} + \mbox{保守性})

といった感じです。
もちろん上記以外にもメンバ変数を隠蔽する理由はあると思います。また最初にも書きましたが、絶対にメンバ変数を隠蔽しなければならない訳でもないので、publicメンバ変数の方が利点が多い場合もあるかと思います。最終的にはチームや個々の判断ですが、これをひとつの参考にして頂ければと思います。

参考文献

  1. 『C++のためのAPIデザイン』 マーティン・レディ 著、ホジソン ますみ 訳、三宅 陽一郎 監修、ソフトバンク クリエイティブ、2011年