読者です 読者をやめる 読者になる 読者になる

変数宣言の位置

C言語では、ローカル変数の宣言を関数の始めに宣言する必要がありました。
これを習慣としているのか、C++ でもローカル変数・オブジェクトの宣言を関数の開始位置で行う人がいます。

//分散を求める関数 (C++スタイル宣言)
int variance(const Array<double>& ad){
assert(ad);
double dSum =0;
int i;
for (i=0; i<ad.size; ++i) dSum +=ad[i];
double dAvr   =dSum/ad.size;
double dSumSq =0;
for (i=0; i<ad.size; ++i){
double dTmp =ad[i] - dAvr;
dSumSq +=dTmp*dTmp;
}
return dSumSq/ad.size;
}
//分散を求める関数 (Cスタイル宣言)
int variance(const Array<double>& ad){
assert(ad);
double dSum   =0;
double dSumSq =0;
double dAvr;
double dTmp;
int i;
for (i=0; i<ad.size; ++i) dSum +=ad[i];
dAvr =dSum/ad.size;
for (i=0; i<ad.size; ++i){
dTmp =ad[i] - dAvr;
dSumSq +=dTmp*dTmp;
}
return dSumSq/ad.size;
}

それでも動作には何の支障もありませんが、変数の「宣言」と「実際に使われ始める位置」が離れてしまうため、プログラムがやや読みづらくなります。 また、上例のCスタイルの方では、2番目のループ内でのみ使われる変数 dTmp のスコープが関数全体に及んでしまうのも、あまり良い状況ではないでしょう。
しかし、C++ が変数・オブジェクトの宣言を関数の任意の位置で行えるようになっているのは、このような可読性に関する (些末な) 問題のためだけではありません。 これには、C にはなかった「クラス」「オブジェクト」が大きく関係しています。

int, double などのプリミティブ (組み込み) 型の変数に加えて、C++ ではクラス型のオブジェクトを扱います。 クラス型のオブジェクトの振る舞いはユーザが (ある程度) 自由に定義することができます。 例えば、「コンストラクタによって初期化することはできるが、それ以降の変更は許可しない」というオブジェクト作ることも不可能ではありません。

class Result {
public:
//construction
Result (const Result&);
Result (double dAvr, double dVar);
//member access (getting)
double Average() const;
double Variant() const;
protected:
double m_dAvr;
double m_dVar;
protected:
//operator
Test& operator = (const Test&);
};
//コピーコンストラクタ
Result::Result(const Result& r)
: m_dAvr(r.m_dAvr), m_dVar(r.m_dVar) {
}
//コンストラクタ
Result::Result(double dAvr, double dVar)
: m_dAvr(dAvr), m_dVar(dVar) {
}
//代入 [forbidden to be used]
Result& Result::operator = (const Result&){
abort();
}
//「平均」の取得
double Result::Average() const {
return m_dAvr;
}
//「分散」の取得
double Result::Variance() const {
return m_dVar;
}

さて、このクラスを使って、最初の例の variance 関数を、分散だけでなく平均の値も返すことができる関数に変更してみましょう。
C++スタイルの宣言を使用するコード は問題なく変更できましたが、Cスタイルの宣言ではこれができません。 関数の開始時点では、コンストラクタに渡す値が決定されていないためオブジェクトを作ることができず、かと言って、これらの値が計算されてからオブジェクトを作るのでは、Cスタイル宣言のルールに違反してしまいます。

//平均・分散を求める関数 (C++スタイル宣言)
int variance(const Array<double>& ad){
assert(ad);
double dSum =0;
int i;
for (i=0; i<ad.size; ++i) dSum +=ad[i];
double dAvr   =dSum/ad.size;
double dSumSq =0;
for (i=0; i<ad.size; ++i){
double dTmp =ad[i] - dAvr;
dSumSq +=dTmp*dTmp;
}
Result res(dAvr, dSumSq/ad.size);
return res;
}
//平均・分散を求める関数 (Cスタイル宣言)
int variance(const Array<double>& ad){
assert(ad);
double dSum   =0;
double dSumSq =0;
double dAvr;
double dTmp;
int i;
Result res; //コンパイルエラー
for (i=0; i<ad.size; ++i) dSum +=ad[i];
dAvr =dSum/ad.size;
for (i=0; i<ad.size; ++i){
dTmp =ad[i] - dAvr;
dSumSq +=dTmp*dTmp;
}
return res;
}

もちろん、この問題を解決する手段はないわけではありません。 ざっと考えただけでも、以下のような方法があげられます。

  • Result オブジェクトの宣言後の値変更を認める。(代入を許可 | Set~系のメンバ関数を追加 | メンバ変数を public 宣言)
  • それぞれ平均, 分散を dounle で返す関数を作り、その戻り値をコンストラクタに渡す。
  • new 演算子を使ってオブジェクトを作成し、そのアドレスを返す。

しかし、これらの方法では、

  • メンバへのアクセス制約を緩めてしまう。
  • 計算の重複が生じる。(平均を二回計算することになる。)
  • 呼び出し側で、返されたアドレスに対して delete を行う必要性がある。

といった不都合が生じてしまい、お世辞にもスマートなやり方とは言えません。

そもそも、こういったやり方を選ぶのであれば、C++ のクラスではなく、C の構造体を使っていれば良いわけです。 クラス, オブジェクトを使ってさえいれば、C++ プログラミング (≒オブジェクト指向) というわけではありません。
C++基本的に C 言語の文法を受け継いではいますが、C とは異なる言語仕様・コーディングの「思想」に基づいて作られています。 これをきちんと理解していない人が書くプログラムは、「C++使ってるけど、やってることはC」になってしまいがち。 新しいパラダイムにシフトする際は、合わない習慣はスッパリと捨て去ることが肝心です。

担当: 成田 (a native C++ writer)