概要

数値計算でのオブジェクト指向のコードで,クラスのメンバ変数のアクセス属性を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の構造体とプログラム内ではほぼ同じ処理をするので,

publicメンバ変数のクラス=構造体

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

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

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

    void x(double u) {
        this->x_ = u;
    }

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

    void y(double v) {
        this->y_ = v;
    }

   vector& operator=(const vector& v);

private:
    double x_;
    double y_;
};

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

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

// yについても同様

このアクセスするためのメンバ関数をアクセッサ(accessor)と一般的に呼ばれています.さらに個別にconst double& x() constゲッター(getter)void x(double u)セッター(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 set(double u, double v) {
       /*
        * u, vどちらも0の場合の例外処理
        */
       const double n = norm_vector(u, v); // ノルムを算出関数
       this->x_ = u / n;
       this->y_ = v / n;
    }

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

private:
    double x_;
    double y_;
};

上記のようにメンバ変数をprivate属性の隠蔽したクラスでは,外部からメンバ変数へ直接アクセスできず,関数setを通じて操作可能です.もちろん関数set内では方向オブジェクトが,必ず1になるように実装されています.
private属性メンバ変数の時とは対照的なメンバ変数を'public'属性で実装した'direction'クラスでは,次のように方向の特性である大きさ1が絶対的に保証されません.

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メンバ変数で実装したクラスの方がメンバ関数が多い分,労力がかかるでしょう.例えばアクセッサなどのメンバ変数を参照しているメンバ関数を修正しなければなりません.

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

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

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

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

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

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

まとめ

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

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

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

参考文献

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

アクセッサですが、参照を戻してしまうと、必ずdouble型の変数を用意しなければいけなくなってしまうので、setterは引数を受け取る形のほうがよいと思います。

//double& x(){
//    return x_;
//}
void set_x(double x) {
   x_=x;
}

また、doubleのような組み込み型では、getterも参照渡しより値渡しのほうがパフォーマンスが良いと思います。

//const double& x()const{
//    return x_;
//}
double get_x()const{
   return x_;
}
61contribution

そもそも最適化もせずにコードのパフォーマンスを比較するのはナンセンスですし,インライン展開された結果同じコードになるものについてパフォーマンスの違いを議論する必要はないのでは?

3379contribution

単純な二つの変数と配列なら、配列にしたほうが遅くなるような気がしますけど。
配列だと添え字アクセスになるので、その分、わずかに変数へのアクセスが遅くなるような気がします。

どっちかというと内部実装をがらっと変える例にした方がいいと思います。
たとえば、vector同士の計算とかを色々増やすに当たって、(x座標,y座標)とするよりも
複素平面での(長さ,角度)で持っている方がいい場合があるのでそっちに変えるとか、
むしろ、はじめからC++標準ライブラリのstd::complexを使ってしまって変数1個に集約し、
複素数の標準関数をそのまま使ったりするとかです。
C++の最適化は優秀なので、コーディング最適化はあまり意味がありません。
(計算量を減らすアルゴリズム最適化は意味がありますけど)
パフォーマンス云々よりも内部実装がしやすいかどうか(その方がコードが短くバグも減る)の方が
大事になると思います。

ただし、今のようにgetterが参照を返す場合、別途値を保持する必要があるので、
値を返すようにしないと、あまりいいコードにならないかもしれません。

74contribution

@hmito さん

ご指摘ありがとうございます。

アクセッサですが、参照を戻してしまうと、必ずdouble型の変数を用意しなければいけなくなってしまうので、setterは引数を受け取る形のほうがよいと思います。

必ずdouble型の変数を用意しなければならないという点が分からないのですが、これに沿うとsetterで引数を受け取る形だと、double型変数を用意しなくても良いと解釈できますが、合っていますか?
また、

getterも参照渡しより値渡しのほうがパフォーマンスが良いと思います。

なぜ値渡しの方がパフォーマンスが良いのでしょうか?

74contribution

@raccy さん

コメントありがとうございます。

単純な二つの変数と配列なら、配列にしたほうが遅くなるような気がしますけど。

配列にする狙いは、コンパイラによる最適化でのSIMD命令によるベクトル演算処理、もしくは、x86系に限ったことですが、SSEやAVX向けの組込関数を使用するためです。SSEで使用するXMMレジスタは、丁度double型(8byte)2つ分なので遅くなることは無いと思います。
コンパイラにもよると思いますが、2要素の配列の添え字アクセスって最適化オプションで許容されませんか?

どっちかというと内部実装をがらっと変える例にした方がいいと思います。

すみません、実際の経験上、思いつくのが今回の例ぐらいなもので。確かにご指摘の通り、全く違う形に変える方が良かったですね。

パフォーマンス云々よりも内部実装がしやすいかどうか(その方がコードが短くバグも減る)の方が
大事になると思います。

全く同感です。今回の伝えたかったことでしたが、書き方が悪かったですかね。

ただし、今のようにgetterが参照を返す場合、別途値を保持する必要があるので、
値を返すようにしないと、あまりいいコードにならないかもしれません。

参照元のオブジェクトが解放された場合やconst_castによる変更でオブジェクト内部の破壊ができるということで、別途値を保持しなければならないという解釈で合っていますか?

3379contribution

getterが参照より値を返したほうがいいのは、例えば角度だけ保持するように
directionを実装し直すと、下記のようなことになるからです。

mazui.cpp
#include <iostream>
#include <cmath>

class direction {
private:
  double angle;
public:
  direction(const double &angle)
    : angle(angle) {
  }
  const double x1() const {
    return std::cos(angle);
  }
  const double y1() const {
    return std::sin(angle);
  }
  const double &x2() const {
    return std::cos(angle);
  }
  const double &y2() const {
    return std::sin(angle);
  }
};

int main() {
  direction direct(std::acos(-1) / 6);
  std::cout << direct.x1() << std::endl;
  std::cout << direct.y1() << std::endl;
  std::cout << direct.x2() << std::endl;
  std::cout << direct.y2() << std::endl;
}

これを、コンパイルして、実行

🐚  g++-5 --std=c++14 -o mazui mazui.cpp
mazui.cpp: In member function 'const double& direction::x2() const':
mazui.cpp:18:26: warning: returning reference to temporary [-Wreturn-local-addr]
     return std::cos(angle);
                          ^
mazui.cpp: In member function 'const double& direction::y2() const':
mazui.cpp:21:26: warning: returning reference to temporary [-Wreturn-local-addr]
     return std::sin(angle);
                          ^
🐚  ./mazui 
0.866025
0.5
fish: Job 1, './mazui ' terminated by signal SIGSEGV (Address boundary error)

上記のようにメソッドから抜けた瞬間ローカル変数は消えちゃうので、プログラムが落ちます。
なので、実装に何らかの工夫が必要になって、値返しに比べると冗長なコードになっちゃうと言うことです。
なお、シェルは気にしないで下さい。

一つ追記
今回はdoubleなので値を返した方がいいという話であって、もし巨大なオブジェクトだったりしたら、
参照で返した方がいい場合もあるので、そこのところは注意しておいて下さい。

74contribution

@raccy さん

directionクラスで角度をメンバ変数とする場合、getterで返すべきは角度だけです。

これはgetterとは言わないですよ。

const double &x2() const {
    return std::cos(angle);
}

それにstd::cosの返り値は、すでに変数angleと別オブジェクトになりますよね。上のコードは、次のコードと同じです。

const double &x2() const {
    double theta = std::cos(angle);
    return theta;
}

ですので、ローカル変数の参照を返して、mainに戻った時点でプログラムが落ちるのは自明かと思います。

また、次のメンバ関数の返り値であるconst doubleconstは不要です。

//  const double x1() const {
//    return std::cos(angle);
//  }

  double x1() const {
      return std::cos(angle);
  }
3379contribution

@Eight さん
ええと、どっから説明すればいいのでしょうか…
メソッドのインターフェースは変えずに中身を変えるってことを説明したかったのですが…

getterやsetterとは言ってますが、メンバー変数と1対1である必要は無く、
むしろ、特定のメンバー変数との紐付けを外すために、関数にしていることが重要なのです。
なので、保持しているメンバ変数が変わっても、メソッドは同じ物を提供しなくてはなりません。
そうじゃないと他のコードも全部変える必要が出てくるからです。
逆に、angleと対になったgetterやsetterは必ずしも作る必要ないのです。
メンバー変数は隠して、全部関数として提供していれば、中身はいかようにも変えられるってのが、
getterやsetterとして作る利点かと思うのですが、違いますでしょうか?

最後のconstは確かにいらないけど、あわせて書いていたので、そのまま付けちゃいました。

74contribution

@raccy さん

意図が分かりました。
ちょっと勘違いしてしまいました、すみません :disappointed:

メンバー変数は隠して、全部関数として提供していれば、中身はいかようにも変えられるってのが、
getterやsetterとして作る利点かと思うのですが、違いますでしょうか?

まさにその通りです。今回、raccyさんが示して頂いたangleに変えて、外部とインターフェイスになる公開しているgetter/setterを変えない場合は、確かに参照を返すのはマズイですね。理解できました、ありがとうございます。