行列とベクトルのクラスに添字演算子([])を多重定義することで、オブジェクトの 要素に添字演算子を使って配列のようにアクセスできるようになります。ただし、添字 演算子の多重定義には注意すべき落とし穴があります。
3.6.1 添字演算子の多重定義
配列で添字演算子([])を使って要素にアクセスできるように、クラスのオブジェク トに対して添字演算子に意味を与えて、行列やベクトルの要素にアクセスできるように します。 行列とベクトルの要素自体はクラス内部に隠蔽されています。添字演算子を介してア クセスすることで添字範囲の逸脱のチェックができます。しかも、クラスを利用するア プリケーション・プログラムから見た外観は、普通の配列と同じように扱えます。
まず、ベクトル・クラスから考えます。次のクラス定義を見てください。
// matrix15.h // Vectorクラスのインターフェース部 class Vector{ friend ostream &operator<<(ostream &, const Vector &); friend istream &operator>>(istream &, Vector &); public: explicit Vector(int = 0); //デフォルトコンストラクタ Vector(const Vector &); //コピーコンストラクタ Vector(const double *, int); // 配列で初期化 ~Vector(); //デストラクタ void setSize(int); //ベクトルのサイズを設定する int getSize() const { return Dim;} //ベクトルのサイズを返す Vector &operator=(const Vector &); //ベクトルを代入する double &operator[](int); // 添え字演算子 private: double *ptr; //ベクトルの先頭要素へのポインタ int Dim; //ベクトルの次元 // ユーティリティ関数 void new_vector();// 領域を確保する void del_vector();// 領域を開放する }; |
添字演算子を多重定義するには、公開メンバ関数に関数 operator[]()を定義し ます。関数プロトタイプ宣言は次のようになります。 double &operator[](int); 引数は添字インデックスを意味します。ここではベクトルのデータ要素番号(添字番 号)になりますから、int型の引数になります。ベクトル・クラスのオブジェクトx について、添字演算 x[10]; に対しては次のメンバ関数が呼び出されます。 x.operator[](10); 戻り値の型が参照型であることに注意しましょう。添字演算子は、要素への代入と 読み出しの2つの意味で使われます。 x[10] = 10.0; // 代入 cout << x[10] << endl; // 読み出し 添字演算子でアクセスするのはベクトル・オブジェクトの要素(動的配列要素)です。 この場合、内部データ(非公開データメンバ)の x.ptr[10] にアクセスします。要 素への参照を返すことにより、要素に値を代入することができます。関数定義は次の ようになります。
// matrix15.cpp の一部分 //多重定義された添え字演算子 double &Vector::operator[](int i) { //添え字の範囲を逸脱していないかチェックする if( i < 0 || i >= Dim ){ cout << "エラー:添え字の範囲を逸脱\n"; abort(); } return ptr[i]; //参照を返して左辺値を作る} |
この関数定義は、内部データにアクセスする前に添字範囲の逸脱についてのエラー チェックを行います。そして内部データの要素 ptr[i] への参照を返します。
同じように行列クラスにも添字演算子を定義します。
// matrix15.h // Matrixクラスのインタフェース部 class Matrix{ friend ostream &operator<<(ostream &, const Matrix &); friend istream &operator>>(istream &, Matrix &); public: explicit Matrix(int = 0, int = 0); // デフォルトコンストラクタ Matrix(const Matrix &); // コピーコンストラクタ ~Matrix(); // デストラクタ void setSize(int, int); // 行列のサイズを設定する int getRow() const { return Row;} // 行数を取得 int getCol() const { return Col;} // 列数を取得 Matrix &operator=(const Matrix &); // 行列を代入する Vector &operator[](int); // 添え字演算子 private: Vector *ptr; // 行ベクトルへのポインタ int Row; // 行の数 int Col; // 列の数 // ユーティリティ関数 void new_matrix(); // 行列の領域を確保する void del_matrix(); // 領域を開放する }; |
添字演算子でアクセスする要素は、行ベクトルつまり Vectorクラスのオブジェクト ですから、operator[]()の戻り値型は Vectorクラスのオブジェクトのへ参照すな わち (Vector &)型です。従って関数定義は次のようになります。
// matrix15.cpp の一部分 //多重定義された添え字演算子 // オブジェクト a の要素に対して a[i][j] で参照、代入が可能になる Vector &Matrix::operator[](int i) { //添え字の範囲を逸脱していないかチェックする if( i < 0 || i >= Row ){ cout << "エラー:添え字の範囲を逸脱\n"; abort(); } return ptr[i]; //参照を返して左辺値を作る } |
行列クラスの添字演算では、内部データの行ベクトルにアクセスします。Matrix クラスのオブジェクトaについて a[5]; でメンバ関数 a.operator[](5); が呼び出されて、行についての添字範囲チェックが行われてから、行ベクトルのオブ ジェクト a.ptr[5] にアクセスします。つまり1つの添字演算子を介して行ベクトル ( Vectorクラス)のオブジェクトの公開メンバ関数を呼び出せます。 また、行列要素にアクセスするには2次元の配列と同様に2つの添字演算子を作用 させます。 a[5][4]; では第6行5列の要素にアクセスします。このとき最初の添字5は行ベクトル要素 (オブジェクト)a.ptr[5]を意味し、2番目の添字4がこの行ベクトルの要素(つ まり成分)a.ptr[5].ptr[4] を意味します。
// lst03_14.cpp // 添字演算子のテスト // Matrix15.cppと一緒にコンパイルする #include <iostream.h> #include "Matrix15.h" int main() { Matrix a(7,5); for(int i = 0; i < a.getRow(); i++) for(int j = 0; j < a.getCol(); j++) a[i][j] = (i+1) + 0.1*(j+1); cout << a << endl; return 0; } |
1.100000e+00 1.200000e+00 1.300000e+00 1.400000e+00 1.500000e+00 2.100000e+00 2.200000e+00 2.300000e+00 2.400000e+00 2.500000e+00 3.100000e+00 3.200000e+00 3.300000e+00 3.400000e+00 3.500000e+00 4.100000e+00 4.200000e+00 4.300000e+00 4.400000e+00 4.500000e+00 5.100000e+00 5.200000e+00 5.300000e+00 5.400000e+00 5.500000e+00 6.100000e+00 6.200000e+00 6.300000e+00 6.400000e+00 6.500000e+00 7.100000e+00 7.200000e+00 7.300000e+00 7.400000e+00 7.500000e+00 |
行列要素に対する操作は、見かけ上は2次元配列の場合と同じです。しかし内部的 には要素へ直にアクセスするのではなくクラスのインターフェースを介して行われま す。内部データそのものはクラスの奥に隠蔽されています。そのためクラス型の行列 では普通の配列と違って、サイズの自由な変更や添字範囲に対するエラーチェックを 備えることができます。
添字演算子の使い方の例として、連立1次方程式を掃き出し法で解いてみます。こ れはガウス・ジョルダン法と呼ばれる最も簡単なアルゴリズムです。次の連立1次方 程式を解きます。 | 1 -2 -2 2 | | x | | 5 | | 2 -2 -3 3 | | y | = | 10 | |-1 6 3 -2 | | z | | 2 | | 1 4 0 -1 | | w | |-10 |, 掃き出し計算では、係数行列が単位行列になるように変形して行き | 1 0 0 0 | | x | | a | | 0 1 0 0 | | y | = | b | | 0 0 1 0 | | z | | c | | 0 0 0 1 | | w | | d |, つまり、 x = a, y = b, z = c, w = d. という解を求めます。この解は、x = 9, y = -2, z = 15, w = 11 になります。 [演習問題 3.6] この掃き出し計算を筆算で求めなさい。(回答はこちら)
// lst03_15.cpp // 連立方程式の解法(ガウス・ジョルダン法) // Matrix15.cppと一緒にコンパイルする #include <iostream.h> #include <iomanip.h> #include "matrix15.h" void Gauss_Jordan(Matrix &); int main() { cout << "連立方程式の解法(ガウス・ジョルダン法)" << endl << endl; // 係数行列 double mat[4][5]={{ 1.0,-2.0,-2.0, 2.0, 5.0}, { 2.0,-2.0,-3.0, 3.0, 10.0}, {-1.0, 6.0, 3.0,-2.0, 2.0}, { 1.0, 4.0, 0.0,-1.0,-10.0}}; Matrix a(4,5);// Matrixクラスのオブジェクトを生成する // 行列の読み込み for(int i = 0; i < a.getRow(); i++) for(int j = 0; j < a.getCol(); j++) a[i][j] = mat[i][j]; cout << "係数行列:" << endl << a << endl; // ガウス・ジョルダン法 Gauss_Jordan(a); cout << endl << "連立方程式の解:" << endl << a << endl; return 0; } // ガウス・ジョルダン法 // 係数行列の掃き出し計算を行い単位行列化した結果を返す void Gauss_Jordan(Matrix &a) { for(int k = 0; k < a.getRow(); k++){ double p=a[k][k];// ピボット係数 int i; for (i = k; i < a.getCol(); i++) a[k][i] /= p; for (i = 0; i < a.getRow(); i++){ // ピボット列の掃き出し if(i != k){ double d = a[i][k]; for(int j = k; j < a.getCol(); j++) a[i][j] -= d * a[k][j]; } } } } |
連立方程式の解法(ガウス・ジョルダン法) 係数行列: 1.000000e+00 -2.000000e+00 -2.000000e+00 2.000000e+00 5.000000e+00 2.000000e+00 -2.000000e+00 -3.000000e+00 3.000000e+00 1.000000e+01 -1.000000e+00 6.000000e+00 3.000000e+00 -2.000000e+00 2.000000e+00 1.000000e+00 4.000000e+00 0.000000e+00 -1.000000e+00 -1.000000e+01 連立方程式の解: 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 9.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 -2.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 1.500000e+01 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 1.100000e+01 |
3.6.3 落とし穴
これで一応はうまくいっています。しかし上の添字演算子の定義は、まだ不完全な 点があります。例えば次のプログラム lst03_16.cpp はコンパイル・エラーになり ます。
// lst03_16.cpp // このプログラムはコンパイルできない!! // (古いコンパイラによってはコンパイルできるかもしれないが) //Matrix15.cppと一緒にコンパイルする #include <iostream.h> #include "Matrix15.h" // ベクトルの内積 double func(const Vector &, const Vector &); int main() { const int size = 4; Vector x(size), y(size); for(int i = 0; i < size; i++){ x[i] = (i+1)*0.1; y[i] = (i+1)*0.2; } cout << func(x, y) << endl; return 0; } // ベクトルの内積を計算する double func(const Vector &x, const Vector &y) { double a = 0.0; for(int i = 0; i < x.getSize(); i++) a += x[i] * y[i]; return a; } |
このプログラムでは、2つのベクトルの内積を計算する関数 func() 内でコンパ イルエラーが発生します。この関数では、引数に2つのオブジェクトを取ります。 内積の計算でオブジェクトを変更することはないので、引数の型は const オブジ ェクトへの参照型となっています。つまり関数に渡されたオブジェクトx、yの型 は const Vector&です。問題を起こすコードは次の添字演算子の部分です。 a += x[i] * y[i]; ここでは次のような添字演算子の呼び出し a += x.operator[]( i ) * y.operator[]( i ); を期待しているのに、実際にはこれらの添字演算子は呼び出されずにエラーとなり ます。 理由は、添字演算子 operator[]()は Vectorクラスのオブジェクトに対して定 義されていますが、const オブジェクトに対しては定義されていないからです。 メンバ関数の呼び出しでは暗黙に thisポインタが渡されます。this ポインタ はメンバ関数を呼び出したオブジェクトのアドレスでした。「x.operator[]( i )」 の呼び出しでは、this の型は「Vector *」です。ところが上のプログラムでは constオブジェクトのポインタ、つまり「const Vector *」型を渡そうとします が、添字演算子は型が違うため、これを受け取ることはできません。 [演習問題 3.7] 上のプログラム lst03_16.cpp をコンパイルしてどのような エラーメッセージが出るか確かめなさい。また、関数 func() の引数を const 参照ではなくて、ただの参照に変更するとコンパイルできること を確かめなさい。
3.6.4 const オブジェクトに対する添字演算子
それではどうすればよいのでしょうか。答えは、const メンバ関数の添字演算子 をもう1つ多重定義します。const メンバ関数は「const Vector *」型の this ポインタを暗黙に受け取ります。つまりこれは const オブジェクトに対して呼び 出すことができるメンバ関数です。そこで次のように、関数を const宣言し、さ らに戻り値型を const 参照型にして演算子を多重定義します。 const double &operator[](int) const;
この関数定義は次のようになります。
//多重定義されたconstオブジェクトに対する添え字演算子 const double &Vector::operator[](int i) const { cout << "constオブジェクトに対する演算子[]が呼ばれました。" << endl; //添え字の範囲を逸脱していないかチェックする if( i < 0 || i >= Dim ){ cout << "エラー:添え字の範囲を逸脱\n"; abort(); } return ptr[i]; } |
ベクトル・クラスについて修正したものを vector03.h 及び vector03.cpp としてまとめて次のようなサンプル・プログラムを使って非constオブジェクト とconstオブジェクトについての添字演算子をテストします。
// lst03_17.cpp // Vecorクラスの添字演算子のテスト // 非constオブジェクトとconstオブジェクト // Vector03.cppと一緒にコンパイルする #include <iostream.h> #include "Vector03.h" int main() { Vector x(5); // オブジェクトxは非const cout << "x[3]に値を代入: " << endl; x[3] = 4.0; cout << "x[3]を参照: " << x[3] << endl; const Vector y = x; // オブジェクトyはconst cout << "y[3]を参照: " << y[3] << endl; cout << "x[3] * y[3] = " << x[3] * y[3] << endl; return 0; } |
x[3]に値を代入: 非constオブジェクトに対する演算子[]が呼ばれました。 非constオブジェクトに対する演算子[]が呼ばれました。 x[3]を参照: 4 constオブジェクトに対する演算子[]が呼ばれました。 y[3]を参照: 4 非constオブジェクトに対する演算子[]が呼ばれました。 constオブジェクトに対する演算子[]が呼ばれました。 x[3] * y[3] = 16 |
オブジェクトxは非constオブジェクトで、オブジェクトyはconstオブジェクト です。添字演算子の関数定義部には、どちらが呼ばれたかが分かるようにメッセー ジ出力のコードを含めています。 行列クラスの添字演算子も、同様に非constオブジェクトに加えて、constオブ ジェクトをサポートするように2つの添字演算子を多重定義することで両方のオブ ジェクトに対して添字演算をサポートできます。 [演習問題 3.8] Matrixクラスについてもconstオブジェクトに対する添字 演算子を多重定義して、上のインタフェース部 matrix15.h と関数定義 部 matrix15.cppを修正しなさい。
3.6.5 「カプセル化」を破る落とし穴
上の添字演算子の実装は、内部データへの参照を返します。これはある意味で、ク ラスのカプセル化(情報隠蔽)を破ることになります。そのため設計するクラスによ っては注意が必要です。添字演算子を使った要素の代入はクラスの外部から内部デー タの変更を許すことになります。しかし、これは行列やベクトルの計算では利用上必 要と考えられるために内部データ(要素)へのアクセスを許しています。ここでのク ラス設計では、カプセル化とは行列やベクトルの内部レイアウト、つまりデータ領域 のメモリ割り当ての実装の詳細やサイズの値(行列やベクトルの次元数)を隠すこと です。 ところが、ここで実装した添字演算子は、内部レイアウトの構造そのものを外部か ら変更されてしまう危険があります。つまりせっかくのカプセル化を壊しています。 具体例を見ましょう。次の例は、クラスを利用するプログラムが行列のレイアウトの 構造を破壊しています。
// lst03_18.cpp // 行ベクトルへの不正な代入(列のサイズが変更されてしまう) // matrix15.cpp と一緒にコンパイルすること #include <iostream.h> #include "matrix15.h" int main() { Matrix a(4,4); // 4行4列の行列 for(int i = 0; i < a.getRow(); i++) for(int j = 0; j < a.getCol(); j++) a[i][j] = 1.0*(i+1) + 0.1*(j+1); Vector x(3); // 3次元ベクトル a[2] = x; // 異なるサイズの行ベクトルを代入 cout << a << endl; return 0; } |
1.100000e+00 1.200000e+00 1.300000e+00 1.400000e+00 2.100000e+00 2.200000e+00 2.300000e+00 2.400000e+00 0.000000e+00 0.000000e+00 0.000000e+00 4.100000e+00 4.200000e+00 4.300000e+00 4.400000e+00 |
このプログラムでは、4行4列の行列aの第3行ベクトル要素 a[2] に違うサイズの、 次元数が3のベクトルxを代入しています。つまり a[2] = x; // 異なるサイズの行ベクトルを代入 では、内部データの行ベクトル・オブジェクト a[2]にベクトル・クラスの代入演算子 を使ってオブジェクトxが代入されます。その結果、もはや4行4列の行列でありま せん。つまり行列のレイアウトは破壊されます。この様にメンバ関数が内部データへ の参照(あるいはポインタ)を返す場合、クラスのカプセル化は容易に破られること になります。 この場合の原因は明らかです。わたしたちのクラスの設計仕様では、異なるサイズ のベクトルの代入を許すようになっているからです。即ち、次のような代入を許して いました。 Vector x(4); // 4次元ベクトル ・・・ Vector y; // サイズ0のベクトル y = x; // 代入:yのサイズは4次元ベクトルになる 従って上の例のような行列のレイアウトの破壊を避ける最も簡単な解決策は、関数の 仕様を変更してサイズが異なるベクトルの代入を禁止することです。
3.6.6 クラスの仕様変更
代入演算子の実装を次のように変更します。
// 多重定義された代入演算子(今後はこのバージョンを使う) // サイズの異なるベクトルの代入を禁止する(再設定しない) Vector &Vector::operator=(const Vector &right) { if(this != &right){ //自己代入をチェックする if(Dim != right.Dim) abort(); // サイズが違う!! for(int i = 0; i < Dim; i++) ptr[i] = right.ptr[i]; //ベクトルの要素をコピーする } return *this; // x = y = zと書けるようにする } |
この仕様変更で、ユーザは次のように、代入する前に明示的にオブジェクトのサイズ を変更する必要があります。 Vector x(4); // 4次元ベクトル ...... Vector y; // サイズ0のベクトル y.setSize( x.getSize() ); // 明示的にサイズをxと同じに変更 y = x; // 代入可能 仕様に一貫性を持たせるために、行列クラスについても、サイズが異なる行列の代 入を禁止します。
//多重定義された代入演算子(今後はこのバージョンを使う) // サイズの異なる行列の代入を禁止する(再設定しない) Matrix &Matrix::operator=(const Matrix &right) { if(this != &right){ //自己代入をチェックする if( (Row != right.Row) || (Col != right.Col) ) abort(); // サイズが違う! for(int i = 0; i < Row; i++)//行ベクトルを代入 ptr[i] = right.ptr[i]; } return *this; // x = y = zと書けるようにする } |
クラスを利用するプログラムでは、明示的にオブジェクトのサイズ変更を行います。 Matrix a(3,4); ........ Matrix b; // 0行0列 b.setSize(a.getRow(), a.getCol()); // サイズ変更 b = a; // 代入可能 これはクラス仕様としては妥当なものと言えます。行列やベクトルの計算では、プロ グラマが次元について意識するのはある意味では当然のことです。行列とベクトルの 掛け算、行列と行列の掛け算の結果、ベクトルや行列の次元はいくらかということは 意識していなければならないことです。この意味で前の仕様は、少しだけコードを 「サボる」ことを許す役割程度の意味に過ぎません(例えば、1次的なオブジェクト をサイズ指定無しで作成してコピーすることで少しだけコードがサボれる)。 より問題なのは、この「サボり」仕様は行列の算術演算において不整合な使用を可 能にします。例えば行列オブジェクトA、Bについて次のような演算を考えてみまし ょう。 A = A * B; ここで演算子*は行列の掛け算として多重定義されているとします。あるいは、同等 な演算*=が定義されていると、次のコードでも書けます。 A *= B; この演算は、行列Bが正方行列(square matrix)のときは問題はありませんが、一 般の行列では不整合な演算を可能にします。仮に、Aをn行m列、Bをm行l列とし た場合。掛け算A*Bは、n行l列の行列となります。これをn行m列のAに代入す ることになります。本来このコードは、行列Aの要素の更新を意味するのに、別の意 味の演算(行列の再設定)を可能にします。以後のクラス設計では、この様な混乱や エラーを招く恐れのある「サボり」仕様を廃することにします。
3.6.7 もう1つの解決策(この方法は今後は採用しない)
もちろん、従来の「サボり」仕様を残した解決策も可能です。それを以下に示しま す。ただし、ここでの方法は後の議論では採用しません。 問題となっているのは、行列オブジェクトaについて、添字演算子を使って列ベク トル要素に違うサイズのベクトルの代入を許したことです。 a[i] = x; // もしベクトルxの次元が違っていたら行列のレイアウトを壊してしまう ですからこの部分のコピーに制限を課すことでカプセル化の穴を塞げばよいことにな ります。そのためにベクトル・クラスのデータメンバにフラグを追加して、フラグの 値によって代入演算子に2通りの振る舞いをさせます。
class Vector{ ・・・・・ public: explicit Vector(int = 0); //デフォルトコンストラクタ Vector(const Vector &); //コピーコンストラクタ Vector(const double *, int); // 配列で初期化 ・・・・・・ void setCopyFlag(bool flag) { CopyFlag = flag;} // 代入フラグを設定 Vector &operator=(const Vector &); //ベクトルを代入する ・・・・・ private: double *ptr; // ベクトルの先頭要素へのポインタ int Dim; // ベクトルの次元 bool CopyFlag;// 代入制限(サイズが違う代入の制御)の有無 ・・・・・・ }; |
非公開(プライベート)部にフラグ CopyFlag を追加します。この非公開データメ ンバの型はブール型(bool)で、これは値 true と false の2つのキーワードを 取ります(このデータ型については後のページで取り上げます)。true(真)の時は サイズの異なるオブジェクトの代入を禁止します。デフォルトでは false(偽)とし、 コンストラクタで初期設定します。
//クラスVectorのデフォルトコンストラクタ Vector::Vector(int dim) : Dim(dim) { ・・・・・ CopyFlag = false; } //クラスVectorのコピーコンストラクタ Vector::Vector(const Vector &init) : Dim(init.Dim) { ・・・・・ CopyFlag = false; } //クラスVectorのコンストラクタ(配列で初期化) Vector::Vector(const double *vec, intdim) : Dim(dim) { ・・・・・・ CopyFlag = false; } |
//多重定義された代入演算子 Vector &Vector::operator=(const Vector &right) { if(this != &right){ //自己代入をチェックする if(Dim != right.Dim) if(!CopyFlag) setSize(right.Dim); // falseならサイズ設定 else { cout << "エラー:不正な代入(サイズが異なる)" << endl; abort(); } for(int i = 0; i < Dim; i++) ptr[i] = right.ptr[i]; //ベクトルの要素をコピーする } return *this; // x = y = zと書けるようにする } |
また、行列オブジェクトをインスタンス化するときには、内部データの行ベクトル についてはフラグを true(真)に設定します。それには次の非公開メンバ関数に1 行追加します。
// 行列の領域を確保する void Matrix::new_matrix() { if(Row == 0 || Col == 0){ Row = 0; Col = 0; ptr = 0; return; } ptr = new Vector[Row]; //行ベクトルの設定 if(ptr == 0){ cout << "エラー:領域確保に失敗しました。\n"; abort(); } for(int i = 0; i < Row; i++){ ptr[i].setSize(Col); ptr[i].setCopyFlag(true); // フラグを設定 } } |
以上のメンバ関数の実装をソースファイル matrix16.cpp にまとめます。 このクラスでは、行列オブジェクトの個々の行ベクトルへの代入に対しては制限が 課せられて、サイズの違うベクトルの代入は禁止されます。一方、行列単位の代入で は、サイズの違う代入(サイズを暗黙に変更して代入)は以前のままサポートされま す。 [演習問題 3.9] このことを上の lst03_18.cpp について確かめなさい。また、 lst03_10.cpp について確かめなさい。
| 目次 | 前のペ−ジ | 次のページ | ページの先頭 |
Copyright(c) 1999 Yamada,K