概要
数値計算でのオブジェクト指向のコードで,クラスのメンバ変数のアクセス属性をpublic
にしているコードをたまに見かけます.特に強制ではないのですが,一般的にメンバ変数はprivate
にします.つまり,そのオブジェクトのメンバ変数は外部から隠蔽されます.これはC++に限ったことではなく,JAVAなどのオブジェクト指向言語ではメンバ変数はprivate
で隠蔽します.
C++入門書の中には,メンバ変数を隠蔽する明確な理由は書かれない事も多く,慣例的な扱いをされます.そのため今回は次の3つの観点からメンバ変数の隠蔽理由を考えていきます.
publicとprivateのメンバ変数のクラス
2次元のベクトルを例にアクセス属性public
とprivate
,それぞれのメンバ変数でのクラスを設計してみます.
まず,public
メンバ変数でのクラスは次のとおりです.
class vector {
public:
double x;
double y;
public:
vector();
vector(const vector& other);
~vector();
vector& operator=(const vector& v);
};
public
メンバ変数は外部から直接参照可能なため,処理が重複しないようにメンバ関数は少ないです.このpublic
メンバ変数のクラスは,C言語やFortranの構造体とプログラム内ではほぼ同じ処理をするので,
という認識でも大丈夫です.
次に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
メンバ変数のパフォーマンスを比較していきます.
vector v;
v.x = x;
v.y = y;
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
メンバ変数を実装した方向のクラスは次のようなものになります.
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が絶対的に保証されません.
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
の例では,座標,座標のそれぞれをdouble
型で実装しましたが,パフォーマンスの観点からメンバ変数はdouble
型配列にした方が良いです.ここでは詳細は省きますが,コンパイラによる最適化の際にベクトル化される可能性があるからです.このパフォーマンスの理由からメンバ変数をdouble
型からdouble
型の配列に変更した場合,コード修正が必要になります.
クラス内でのコード修正は,最初で示したpublicとprivateのメンバ変数のクラスのとおり,private
メンバ変数で実装したクラスの方がメンバ関数が多い分,労力がかかるでしょう.例えばアクセッサなどのメンバ変数を参照しているメンバ関数を修正しなければなりません.
void vector::x(double u) {
// this->x_ = u; // 修正前
this->x_[0] = u; // 修正後
}
しかし,public
メンバ変数ではクラスの外部にて直接参照されているため,すべての箇所でコード修正が必要になります.
vector v;
// v.x = x;
// v.y = y;
// コードを書き直す必要がある.
v.x[0] = x;
v.x[1] = y;
一方,private
メンバ変数では,アクセッサの関数名が変わるわけではないのでコード修正は不要です.
vector v;
// コードを書き直す必要はない.
v.x(u);
v.y(v);
メンバ変数の変更によるコード修正は,private
メンバ変数のときはクラス内だけで局所的,public
メンバ変数では散逸的になります.修正をする際はやはり前者の局所的になっている方が簡単ですし,散逸的に修正が必要では探すのにも手間だったりしますし,参照箇所が多いとかなり大きな作業となるでしょう.この保守性の場合も,private
メンバ変数の方が利点が多いと思います.
まとめ
今回,メンバ変数のアクセス属性において,パフォーマンスの点ではpublic
メンバ変数,整合性,保守性の面ではprivate
メンバ変数が優位でした.それぞれのメンバ変数でパフォーマンスの優位よりも整合性と保守性での優位の方がとても大きく
といった感じです.
もちろん上記以外にもメンバ変数を隠蔽する理由はあると思います.また最初にも書きましたが,絶対にメンバ変数を隠蔽しなければならない訳でもないので,public
メンバ変数の方が利点が多い場合もあるかと思います.最終的にはチームや個々の判断ですが,これをひとつの参考にして頂ければと思います.
参考文献
- 『C++のためのAPIデザイン』 マーティン・レディ 著,ホジソン ますみ 訳,三宅 陽一郎 監修,ソフトバンク クリエイティブ,2011年
アクセッサですが、参照を戻してしまうと、必ずdouble型の変数を用意しなければいけなくなってしまうので、setterは引数を受け取る形のほうがよいと思います。
また、doubleのような組み込み型では、getterも参照渡しより値渡しのほうがパフォーマンスが良いと思います。
そもそも最適化もせずにコードのパフォーマンスを比較するのはナンセンスですし,インライン展開された結果同じコードになるものについてパフォーマンスの違いを議論する必要はないのでは?
単純な二つの変数と配列なら、配列にしたほうが遅くなるような気がしますけど。
配列だと添え字アクセスになるので、その分、わずかに変数へのアクセスが遅くなるような気がします。
どっちかというと内部実装をがらっと変える例にした方がいいと思います。
たとえば、vector同士の計算とかを色々増やすに当たって、(x座標,y座標)とするよりも
複素平面での(長さ,角度)で持っている方がいい場合があるのでそっちに変えるとか、
むしろ、はじめからC++標準ライブラリの
std::complex
を使ってしまって変数1個に集約し、複素数の標準関数をそのまま使ったりするとかです。
C++の最適化は優秀なので、コーディング最適化はあまり意味がありません。
(計算量を減らすアルゴリズム最適化は意味がありますけど)
パフォーマンス云々よりも内部実装がしやすいかどうか(その方がコードが短くバグも減る)の方が
大事になると思います。
ただし、今のようにgetterが参照を返す場合、別途値を保持する必要があるので、
値を返すようにしないと、あまりいいコードにならないかもしれません。
@hmito さん
ご指摘ありがとうございます。
必ずdouble型の変数を用意しなければならないという点が分からないのですが、これに沿うとsetterで引数を受け取る形だと、
double
型変数を用意しなくても良いと解釈できますが、合っていますか?また、
なぜ値渡しの方がパフォーマンスが良いのでしょうか?
@raccy さん
コメントありがとうございます。
配列にする狙いは、コンパイラによる最適化でのSIMD命令によるベクトル演算処理、もしくは、x86系に限ったことですが、SSEやAVX向けの組込関数を使用するためです。SSEで使用するXMMレジスタは、丁度
double
型(8byte)2つ分なので遅くなることは無いと思います。コンパイラにもよると思いますが、2要素の配列の添え字アクセスって最適化オプションで許容されませんか?
すみません、実際の経験上、思いつくのが今回の例ぐらいなもので。確かにご指摘の通り、全く違う形に変える方が良かったですね。
全く同感です。今回の伝えたかったことでしたが、書き方が悪かったですかね。
参照元のオブジェクトが解放された場合や
const_cast
による変更でオブジェクト内部の破壊ができるということで、別途値を保持しなければならないという解釈で合っていますか?getterが参照より値を返したほうがいいのは、例えば角度だけ保持するように
directionを実装し直すと、下記のようなことになるからです。
これを、コンパイルして、実行
上記のようにメソッドから抜けた瞬間ローカル変数は消えちゃうので、プログラムが落ちます。
なので、実装に何らかの工夫が必要になって、値返しに比べると冗長なコードになっちゃうと言うことです。
なお、シェルは気にしないで下さい。
一つ追記
今回はdoubleなので値を返した方がいいという話であって、もし巨大なオブジェクトだったりしたら、
参照で返した方がいい場合もあるので、そこのところは注意しておいて下さい。
@raccy さん
direction
クラスで角度をメンバ変数とする場合、getterで返すべきは角度だけです。これはgetterとは言わないですよ。
それに
std::cos
の返り値は、すでに変数angle
と別オブジェクトになりますよね。上のコードは、次のコードと同じです。ですので、ローカル変数の参照を返して、mainに戻った時点でプログラムが落ちるのは自明かと思います。
また、次のメンバ関数の返り値である
const double
のconst
は不要です。@Eight さん
ええと、どっから説明すればいいのでしょうか…
メソッドのインターフェースは変えずに中身を変えるってことを説明したかったのですが…
getterやsetterとは言ってますが、メンバー変数と1対1である必要は無く、
むしろ、特定のメンバー変数との紐付けを外すために、関数にしていることが重要なのです。
なので、保持しているメンバ変数が変わっても、メソッドは同じ物を提供しなくてはなりません。
そうじゃないと他のコードも全部変える必要が出てくるからです。
逆に、angleと対になったgetterやsetterは必ずしも作る必要ないのです。
メンバー変数は隠して、全部関数として提供していれば、中身はいかようにも変えられるってのが、
getterやsetterとして作る利点かと思うのですが、違いますでしょうか?
最後のconstは確かにいらないけど、あわせて書いていたので、そのまま付けちゃいました。
@raccy さん
意図が分かりました。
ちょっと勘違いしてしまいました、すみません
まさにその通りです。今回、raccyさんが示して頂いた
angle
に変えて、外部とインターフェイスになる公開しているgetter/setterを変えない場合は、確かに参照を返すのはマズイですね。理解できました、ありがとうございます。