第5回 C++・演算子のオーバーローディング、
ジェネリックプログラミング
および簡単なテンプレート


変数のいろいろ

項目 参照変数(reference variables) ポインタ変数(pointer variables) 通常の変数(ordinary variables)
例1 (example)
int var = 0;
int &i_ref {var};/* 参照変数*/
const double& d_ref {1};/* 参照変数 */


(NG) int &x_ref; /* 定義のみはNG */
(NG) double &xd_ref=1; /*const必要 */
int var = 0;
int *iq = &var;
int q = *iq;
iq++;/* pointer increment */
char *ip;/* 定義のみもOK */

char **ppc; 
/* pointer to pointer to char */

int *ap[15]; 
/* array of 15 pointers to int */

int (*f)(char *);
/* pointer to funtion 
  with a char* argument, 
  returning int */
  
int *f(char *);
/* function with a char* 
  argument, 
  returing a pointer to int */

int var = 0;
char c = 'a';
int array[20];
関数の引数(parameters) アドレス(address) アドレス(address) 値(value)
例2 (example)
void swap(int&x, int&y);
template <typename> t;
void swap(T&a, T&b);

void f(string&s, vector <int>&v);
const string& title() 
  const {return _title;}
void swap(int*x, int*y);
void fp(char *p);
void fr2(char &r)
{
	char *p=&r;
	while (*p)
		cout << *p++;
}

void f(vector <Shape*> shape);
vector <double> *d;
vector <Shape*> vs;
Shape *shape;
shape->r = 0.0;
Circle *circle = new 
   Circle(r,g,b,center, radius);
shape = circle;/* override */
/* class Circle: public Shape {}*/
circle->Shape::r = 1.0;

vector <string> result;
vector <vector <double>> m;

Shape shape;
shape.r = 0.0;
Circle circle;
/* class Circle: public Shape {}*/
circle.Shape::r = 1.0;
built-in type 勧めない(not recommended) OK OK
欠点
一度代入したら、値を変更できない

built-in typeでは関数で何が起きるか
把握していないと値が急に変更される
ポインタアドレスは変更OKだが、
動的にポインタ用のメモリ割当ると
自分でdeleteが必要
関数の引数では重たい


クラス継承あれこれ

基底クラスと派生クラスを使った、データのやりとりの基本として、 基底クラスと派生クラスの定義、1引数以上のパラメータが基底クラスにあるとした場合、 派生クラスのコンストラクタから、基底クラスのコンストラクタをアクセスする方法、 ならびに、基底クラス(のポインタ)に派生クラス(のポインタ)を代入した後、 基底クラスと派生クラスでオーバーライドされる関数がある場合の動作方法例をまとめておきます。クラスの継承は、最近はやりの大抵の言語に共通の特徴であり、とても重要な概念でることがわかると思います。

C++ Java Python
#include <iostream>
usina namepsace std;
#include <string>
class Base {
    T1 a; T2 b;
public:
    Base(T1 a, T2 b):a(a),b(b){}
    virtual void method(){ 
      cout << "Base:" << a << " "
           << b << endl;
    }
};
class Derived : public Base {
    T3 c;
public:
    Derived(T1 a, T2 b, T3 c):
        Base(a,b), c(c) {}
    void method(){
      cout << "Derived:"<< c << endl;
      Base::method();
    }
};
int main(){
    Derived* My = new Derived(1,2,3);
    Base *Thy = new Base(10,20);
    Thy = My;
    Thy->method();
    return 0;
}
	
public class Base {
    private T1 a; 
    private T2 b;
    public Base(T1 a, T2 b){
        this.a = a;  
        this.b = b;
    }
    public void method(){ 
        System.out.println("Base:"+a+ " "+b);
    }
}
public class Derived extends Base {
    private T3 c;
    public Derived(T1 a, T2 b, T3 c){
        super(a,b);
        this.c = c;
    }
    public void method(){
        System.out.println("Derived:"+c);
        super.method();

    }
    public static void main(String[] args){
      Derived My = new Derived(1,2,3);
      Base Thy = new Base(10,20);
      Thy = My;
      Thy.method();
    }
}
class Base:
   def __init__(self,a,b):
        self.a = a
        self.b = b
   def method(self):
        print(" Base: ",self.a," ",self.b)
class Derived(Base):
    def __init__(self,a,b,c):
        Base.__init__(self,a,b)
        self.c = c
    def method(self):
        print("Derived: ",self.c)
        Base.method(self)

if __name__ == '__main__':
    My = Derived(1,2,3)
    Thy = Base(10,20)
    Thy = My
    Thy.method()
	


派生クラスと基底クラスの仮想関数の使い分け

前回、仮想関数による、派生クラスの仮想化の例を示しました。 そこでは、基底クラスのポインタからvirtualで宣言された派生クラスを呼び出すと、 派生クラスが複数あるときに、 それが、どの派生クラスであるかを意識せずに、 いろいろな仮想関数を必要に応じて呼び出されるため、 使い方によっては非常に便利であることを実例で紹介しました。 もし、そのような仮想関数が純粋仮想関数の場合は、 派生クラスで必ず実体を定義しなければなりませんが、 仮想関数が「純粋」でない場合、 基底クラスと派生クラスをうまく使い分けすれば、 それはまた便利な使い方ができます。 使い分けの例としては、プログラム4-4で、 呼び出したい基底クラスの、クラス名::関数名で呼び出せることを紹介しました。 別の例で、もう一度、この事例を紹介しておきます。
EVcarMain2.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* プログラム5-1 電気自動車に関するmain関数での実施例(明示的な関数呼び出し)*/
#include "EVcar.h"
int main(){
	Car *base = new Car(2004, 54000.0);/* オブジェクト1 */
	base->setPrice(1500000.0);/* 価格の設定 */
	cout << "☆派生クラス☆ ";
	EVcar ev1("Litium", "CHAdeMO");/* 電気自動車クラスのインスタンス生成 */
	ev1.setChargeTime(240.0);/* 4時間の充電時間を設定 */
	ev1.setVoltage(200.0);/* 200Vに設定 */
	ev1.setPrice(2000000);/* 基底クラスの価格を設定 */
	ev1.print();/* 派生クラスのprint */
	cout << "★基底クラス★ ";
	Car *car = base;/* まず基底クラスのプリント */
	car->print();/* 基底クラスのprintメソッドの呼出し */
	cout << endl << "仮想関数で基底クラスのポインタに派生クラスを代入しprintを呼出すと?"<< endl;
	car = &ev1;/* 基底クラスのポインタ変数に派生クラスのアドレスを代入 	*/
	car->print();/* 基底クラスのポインタだが派生クラスのprintメソッドの呼出し */
	cout << endl <<	"明示的に基底クラスのprintを呼出すと?"<< endl;
	car->Car::print();/* 基底クラスのprintメソッドを明示的に呼出す */
	return 0;
}
19行目にあるように、

car->print();

の代わりに、基底クラス名を名前空間のように明示的に関数の前につけて

car->Car::print();

とすれば、基底クラスの関数を明示的に呼び出すことができます。


単回帰モデル

クラスの継承をひとまず離れて、以前の課題と関連する単回帰を行うモデルを紹介します その課題とは、food.csvデータを読み込んで、食べ物のカロリー値を、GI値, タンパク質, 脂質, 炭水化物などから線形回帰モデルで予測しよう、というものでした。その際、説明変数が1個でした。 そこでのサンプルプログラムをこちらにおきますので 課題の解答例として利用してください。


重回帰モデル

単回帰の延長線上で重回帰を行うモデルを紹介します。 現実的なサンプルデータでは、説明変数が複数個の場合が多く、いまk個あるとします。目的変数の予測値をy で表現すると以下のように記述できます。



一方、以下の行列を定義します。



これは、(説明変数+1)=(k+1)次元のデータで、サンプル数(n)の行数からなります。 係数はベクトルで以下のように表現できます。



最小二乗法を適用すると、係数ベクトルは、観測された目的変数y (訓練データ)から以下のように表現されることが知られています。



以下は、逆行列をガウス消去法を利用して計算するシンプルなプログラムです。 文献としては、Wikipediaのガウス消去法がお勧めです。 あまり、いろいろな例外的な処理は含みませんが、普通の正則な正方行列であれば、動作すると思います。プログラムの頑強性は高くないので注意ください。なお、逆行列計算の結果は、Matrixクラスへのポインタとして実装しています。
myInverse.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/* プログラム5-2 逆行列計算 */
#include <iostream>
#include <cmath>
using namespace std;

#include "myMatrix.h"

/* i <= p < row においてv[p][i] != 0 となる最小の整数pを返す関数 */ 
int 
Matrix:: smallestInteger ( int i, Matrix &A ) {
    for ( auto p = i ; p < row ; p++ ){
        if (A.v[p][i] != 0.0) return(p);
    }
    return(-1);
}

/* 行列の(行)EiとEjを交換する関数 */
void 
Matrix::swapRow ( int i, int j, Matrix &A, Matrix &B ) {
    if ( i < 0 || j < 0 || i >= col || j >= col ){
        cerr << i << "行と" << j << "行を交換できません。" << endl;;
        return;
    }
    vector <double> tmpa(col);
    vector <double> tmpb(col);
    for ( auto k = 0 ; k < col ; k++ ){
        tmpa[k] = A.v[j][k];
        tmpb[k] = B.v[j][k];
        A.v[j][k] = A.v[i][k];
        B.v[j][k] = B.v[i][k];
        A.v[i][k] = tmpa[k];
        B.v[i][k] = tmpb[k];
    }
    return;
}
 
/* ガウス消去法による逆行列解 */
/*   入力:
     A: nxnの行列の中身 (2次元実数配列)
     出力:
     C: nxnの逆行列(解)
     戻り値:
     0: 正常終了(解あり)
     -1: 異常終了(解なし)
*/
int 
Matrix::GaussElimination ( Matrix &A, Matrix &C ) {
	double sum = 0.0;
	double pivot = 0.0;

	if ( row != col || row < 1 ){
		if ( row < 1 )
			cerr << row << "(<1)次行列の逆行列は解けません。" << endl;
		else 
			cerr << "正方行列でないので逆行列は求まりません。" << endl;
		return -1;
	}

	Matrix B(row, row);//一時的な利用
	
	/* 行列(b)の初期化 */
	for ( auto i = 0 ; i < row ; i++ ) for ( int j = 0 ; j < row ; j++ ) 
		if (i == j) B.v[i][j] = 1.0;
		else B.v[i][j] = 0.0;
	/* 消去フェーズ */
	for ( auto k = 0 ; k < row-1 ; k++ ){
		int p = smallestInteger( k, A );
		if (p < 0){
			cerr << "方程式の解はありません。" << endl;
			return -1;
		}
		if ( p != k ) {
			swapRow( p, k, A, B );
		}
		for ( auto i = k+1 ; i < row ; i++ ){
			pivot = A.v[i][k] / A.v[k][k];/* ピボットの選択 */
			for ( int j = 0 ; j < row ; j++ ){
				A.v[i][j] -= pivot * A.v[k][j];
				B.v[i][j] -= pivot * B.v[k][j];
			}
		}
	}

   /* 後方置換フェーズ */
    for ( auto i = row-1 ; i >= 0 ; i-- )
		for ( auto j = 0 ; j < row ; j++){ 
        sum = 0.0;
        for ( auto k = i+1 ; k < row ; k++ ){
            sum += A.v[i][k] * C.v[k][j];
        }
        C.v[i][j] = (B.v[i][j] - sum)/A.v[i][i];
    }
    return 0;
}

Matrix*
Matrix::inverse(){
	Matrix *X = new Matrix(row, row);
	int result = this->GaussElimination(*this, *X);
	if (result)
		cerr << "逆行列は存在しません" << endl;
	return(X);
} 
以下は、回帰クラス(Regression class)の実装例です。重回帰を表現できるように、得られる回帰係数(coef)はvector型にしています。
Regression.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* プログラム5-3 回帰モデル用クラス*/
#include <iostream>
using namespace std;
#include <string>
#include <vector>

class Regression {
	private:
		int samples; /* サンプル数 */
		int variables;/* 説明変数の数 */
		vector <vector <double>> data;//  サンプル数 x 変数(観測データ)
		vector <double> labels; // 目的変数【ラベル】
		vector <double> mean; // 説明変数の平均値(計算で使用)
		vector <double> coef; // 訓練された重回帰係数
		vector <double> predicted; // 予測値
		vector <string> name; // 名前
		double ym; /* mean of response variable */
		double yp; /* mean of predicted response variable */
		double R2;/* 寄与率 */
	public:
		Regression(vector <vector <double>> data, vector <double> labels);
		~Regression(){}
		int getSamples(){ return samples;} // サンプル数を返す
		int getVariables(){ return variables;}// 説明変数の数を返す
		vector <double> getLabels(){return labels;};
		vector <double> getMean(){ return mean;};
		vector <vector <double>> getData() { return data;}
		void computeMean();/* 平均値を一括で計算 */
		vector * doRegression();/* 重回帰モデルを計算 */
		double computeR2();/* 寄与率の計算 */
		double predictValue(vector <double> unknown );/* 未知データから予測 */
}; 
#endif
以下は回帰クラスで定義されたメンバー関数の実装例です。 回帰係数は、最初に述べた行列計算で求めています。 寄与率(R2)は、課題で述べたように以下の式を実装したものです。



Regression.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/* プログラム5-4 回帰クラスのメンバー関数 */
#include "Regression.h"
#include "myMatrix.h"
#include <cstdlib>

/* コンストラクタ*/
Regression::Regression(vector <vector <double>> data, vector <double> labels){
	this->data = data;
	this->labels = labels;
	samples = (int) data.size();
	vector <double> item = data[0];
	variables = (int)item.size();
	cout << "【コンストラクタ】[Multiple Regression] : samples = " << 
		samples << " variables = " << variables << endl;
}

void
Regression::computeMean( ){// 平均値計算
	vector <double> sample;
	double response;
	float GI=0, carbon = 0, fat = 0, protein=0, calorie=0;
	for (auto i = 0 ; i < samples;i++){
		/* 各サンプルデータで */
		sample = data[i];/* GI, carbon, and fat in this order */
		response = labels[i];/* calorie */
		GI += sample[0];/* GI */
		carbon += sample[1];/* 炭水化物 */
		fat += sample[2];/* 脂質 */
		protein += sample[3];/* タンパク質 */
		calorie += response;
	}
	GI /= (float)samples;
	carbon /= (float)samples;
	fat /= (float)samples;
	protein /= (float) samples;
	calorie /= (float)samples;
	this->ym = calorie;
	this->mean.push_back(GI);
	this->mean.push_back(carbon);
	this->mean.push_back(fat);
	this->mean.push_back(protein);
}

vector * 
Regression::doRegression(){
	
	computeMean();// 平均値の計算 

	Matrix *m = new Matrix(samples, variables+1);
	vector <double> sample;
	vector <double> v(samples);

	for (auto i = 0 ; i < (int)data.size();i++){
		/* 各サンプルデータで */
		
		m->v[i][0] = 1.0;
		m->v[i][1] = data[i][0] - mean[0]; /* GI */
		m->v[i][2] = data[i][1] - mean[1]; /* carbon */
		m->v[i][3] = data[i][2] - mean[2]; /* fat */
		m->v[i][4] = data[i][3] - mean[3]; /* protein */
		v[i] = labels[i] - ym;/* calorie */
	}
	// w = (t(m)*m)^{-1} * t(m)* coef
	Matrix *m1 = m->transpose();//転置
	Matrix *m2 = m1->multiply(m);//乗算
	Matrix *m3 = m2->inverse();//逆行列
	Matrix *m4 = m3->multiply(m1);//乗算
	vector <double> *weight = m4->multiply(v);//乗算
	this->coef = *weight;
	vector <double> *pred = m->multiply(coef);//乗算
	this->predicted =*pred;//予測値
	this->yp = 0.0;
	for (auto i = 0 ; i < (int) predicted.size();i++)
		this->yp += predicted[i];
	this->yp /= (float) predicted.size();//予測値の平均
	delete m;
	delete m1;
	delete m2;
	delete m3;
	delete m4;
	return(weight);
}

double
Regression:: computeR2(){// 寄与率計算
	/* ground truth from "labels" */
	double numerator = 0.0;
	double denom1 = 0.0, denom2 = 0.0;	
	for (auto i = 0 ; i < samples; i++){
		numerator += (labels[i] - ym)  *  (predicted[i] - yp);
		denom1 += (labels[i]    - ym)  *  (labels[i]    - ym);
		denom2 += (predicted[i] - yp)  *  (predicted[i] - yp);
	}
	R2 = numerator * numerator / (denom1 * denom2);
	return(R2);
}

double 
Regression::predictValue( vector  unknown ){// 説明変数から目的変数を予測 
	if (variables != (int) unknown.size()){
		cerr << "説明変数の次元:" << variables <<
			"と目的(独立)変数の数:"<< unknown.size()
			<< "が一致しません" << endl;
	}
	double result = coef[0];
	for (auto i=0 ; i < (int) unknown.size();i++)
		result += unknown[i] * coef[i+1];
	return result;
}
メイン関数は以下のとおりです。 CSVデータを読み込み、Foodクラスのオブジェクトを作ります。 その後、説明変数(variables)や目的変数(labels)をvector型にpush_backしていきます。 説明変数は全体として、dataという名前の個々の要素がdoubleのvector型のvector型に加えています。実際は、4個(GI値, 炭水化物, 脂質, たんぱく質)のdoubleをまとめたvector型のデータが(可変の)サンプル数だけあり、それをすべて保持するようにしています。
RegressionMain.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/* 5-5 重回帰による、目的変数の訓練と予測 */
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <cstdlib>
using namespace std;
#include <vector>
#include "Food.h"
#include "Regression.h"
#include 

auto myHeader(){
    time_t now = time(nullptr);
    struct tm* local = localtime(&now);
    cout << "*******************************************" << endl;
    cout << "作者:青野雅樹, 01162069" << endl;
  	cout << "日付:" << local->tm_year+1900 << "年" << local->tm_mon+1 << "月" <<
  		local->tm_mday << "日" << local->tm_hour << "時" <<
  		local->tm_min << "分" << local->tm_sec << "秒" << endl;
    cout << "内容:重回帰モデルでの機械学習(訓練・予測)" << endl;
    cout << "*******************************************" << endl;
    return now;
}

void trim(string& str){/* 文字列の両端のspaceを削除 */
	string::size_type pos = str.find_last_not_of(' ');
	if (pos != string::npos ) {
		str.erase(pos + 1);
		pos = str.find_first_not_of(' ');
		if ( pos != string::npos ) str.erase(0, pos);
	}
	else str.erase( str.begin(), str.end() );
}

vector <string> split(string& input, char delimiter){
    istringstream stream(input);/* 入力文字列をistringstreamとして処理 */
    string field;
    vector <string> result;/* 結果の文字列リストを保持 */
    while (getline(stream, field, delimiter)) {
        ::trim(field);/* 両端のスペースを削除 */
        result.push_back(field);/* データをvectorにpush */
    }
    return result;
}

void 
readData(string fileName, vector <Food*> &foodList){
    ifstream fin(fileName.c_str(), ios::in);
    if (fin.fail()){/* ファイルオープンできないとき */
        cerr << "ファイル:" << fileName << "がオープンできませんでした" << endl;
        exit(EXIT_FAILURE);/* 終了:#include を必要とする */
    }
    string name;
    float GI, carbon, calorie, fat, protein;
    string line;
    getline(fin, line);//skip
    do {
	if (!getline(fin, line)) break;
	vector <string> s = split(line,',');
	name = s.at(0);
	GI = stof(s.at(1));
	carbon = stof(s.at(2));
	calorie = stof(s.at(3));
	fat = stof(s.at(4));
	protein = stof(s.at(5));
	Food *food = new Food(name,GI,carbon,calorie,fat, protein);
	foodList.push_back(food);
    } while (true);
    fin.close(); /* ファイルのclose */
}

int main(int ac, char **av){
    /*ファイル名が指定されていることを確かめる*/
    if (ac != 2){/* 引数が1個でないとき */
        cerr << "RegressionMain [food CSVデータファイル名]" << endl;
        return -1;
    }

    ::myHeader();//ヘッダーのプリント

    vector <Food*> list;
    cout << "【CSVデータ読込】: " << av[1] << endl;
    readData(av[1], list);
    vector <vector <double>> data;
    vector <double> variables;
    vector <double> labels;
    vector <string> name;
    for (auto i = 0 ; i < (int)list.size(); i++){//データをvector型に追加
	Food *food = list[i];
	name.push_back(food->getName());
	variables.push_back(food->getGI());
	variables.push_back(food->getCarbon());
	variables.push_back(food->getFat());
	variables.push_back(food->getProtein());
	labels.push_back(food->getCalorie());
	data.push_back(variables);
	variables.clear();
    }

    Regression regress(data, labels);//回帰クラスのオブジェクト生成
    cout << "【重回帰】オブジェクト生成...." << endl;
    cout << "【説明変数】サイズ = " << regress.getVariables() << endl;
    cout << "【データ】サイズ = " << data.size() << endl;

    vector <double>* result = regress.doRegression();
    cout << "【訓練された回帰係数】" << endl;
    cout << " カロリー=(0)+GI*(1)+炭水化物*(2)+脂質*(3)+タンパク質*(4)"<< endl;
    for (auto i = 0 ; i < (int)result->size(); i++)
	cout << "\t(" << i << ")= " << (*result)[i] << " " << endl;
	
    double R2 = regress.computeR2();
    cout << "【寄与率】R2 = " << R2 << endl;

    vector <double> peanut = {28, 19.6, 49.4, 26.5};//585
    vector <double> tofu = {42, 2, 3, 4};//56
    vector <double> shiitake = {28, 4.9, 0.4, 3};//18
    cout << "カロリー予測 (ピーナツ) = " << regress.predictValue(peanut) << endl;
    cout << "カロリー予測 (絹豆腐) = " << regress.predictValue(tofu) << endl;
    cout << "カロリー予測 (しいたけ) = " << regress.predictValue(shiitake) << endl;

    delete result;
    return 0;
}
実行結果は、以下のようです。

$ make run
./RegressionMain food.csv
*******************************************
作者:青野雅樹, 01162069
日付:2019年7月9日10時59分57秒
内容:重回帰モデルでの機械学習(訓練・予測)
*******************************************
【CSVデータ読込】: food.csv
【コンストラクタ】[Multiple Regression] : samples = 49 variables = 4
【重回帰】オブジェクト生成....
【説明変数】サイズ = 4
【データ】サイズ = 49
【訓練された回帰係数】
 カロリー=(0)+GI*(1)+炭水化物*(2)+脂質*(3)+タンパク質*(4)
        (0)= 1.23195e-05
        (1)= 0.0926673
        (2)= 3.97252
        (3)= 8.97857
        (4)= 4.72688
【寄与率】R2 = 0.998902
カロリー予測 (ピーナツ) = 649.26
カロリー予測 (絹豆腐) = 57.6803
カロリー予測 (しいたけ) = 39.8321

コンストラクタのオーバーローディング

関数のオーバーロードは以前、紹介しました。 このうち、コンストラクタのオーバーロードに関して、 C++では幾つかの簡略記法が使えますので追記しておきます。 以下の例を見てください。


class complex {
	double re, im;/* 実部と虚部 */
public:
	complex(): re(0), im(0) {}
	complex(double r) : re(r), im(0) {}
	complex(double r, double i): re(r), im(i) {}
	//...
};

この例では、複素数クラスのコンストラクタのオーバーロードとして、 無引数、1引数、2引数の3種類が登場します。 これらを初期化リストだけで制御しています。 実は、この3つのコンストラクタをひとつにまとめることができます。 このために、以下のように書きます。


class complex {
	double re, im;/* 実部と虚部 */
public:
	complex(double r=0, double i=0): re(r), im(i) {}
	//...
};

また、1引数コンストラクタは明確に呼び出す必要はなく、 簡略表現ができます。


complex b = 3;

と書いて、


complex b = complex(3);

の意味で表現することができます。 また、C++11から導入された{ }による初期化(uniform initialization)により


complex b1 = {2, -1};// complex b1(2, -1);
complex b2 {2, -1};// complex b2(2,-1);

のような記法も可能となっています。


演算子のオーバーローディング

C++では、コンストラクタやその他クラスのメンバー関数を多重定義(オーバーロード)できることをすでに述べました。これらはJava言語にも存在するオブジェクト指向言語のポリモルフィズムの一例として重要な概念であることも述べました。 今回は、関数のオーバーロードとは別に、Java言語になくてC++言語にある便利な機能を紹介します。 それは、演算子のオーバーロード(operator overloading) (Operator Overloading)です。 C++には、C言語やJava言語と同様に多数の演算子が用意されています。 たとえば四則演算子、比較演算子、代入演算子などがその代表です。 以下では、複素数と行列を例にとって、演算子のオーバーローディングに関して例示します。 実は、すでにcoutにおいて、<<という演算子を出力用に用いており、 これは左ビットシフト演算子でもありますので、演算子のオーバーロードを知らず知らず使ってきています。

C++の演算子のオーバーロードが可能な演算子は以下の通りです。

+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= <= |= << >> >>= <<= == != <= >= && || ++ -- ->* , -> [] () new new[] delete delete[]


三項演算子( ? : )や、sizeof, typeidなどの演算子はオーバーロードできませんので注意してください。 また、
演算子のオーバーローディングでは、元の演算子が単項演算子なら単項演算が再定義でき、 二項演算子なら二項演算が定義できます。それ以外の用途には使えません。
ので、注意してください。

まずは、複素数の例での演算子のオーバーロードの例を示します。


class complex {
	double re, im;/* 実部と虚部 */
public:
	complex(double r=0, double i=0): re(r), im(i) {}
	complex operator + (complex b){ /* 加算 */
		complex a = *this;
		complex c;
		c.re = a.re + b.re; 
		c.im = a.im + b.im; 
		return(c);
	}
	complex operator * (complex b){	/* (a+bi)*(c+di) = (a*c-b*d)+(c*d+b*c)i */
		complex a = *this;
		complex c;
		c.re = a.re * b.re - a.im * b.im;
		c.im = a.re * b.im + a.im * b.re;
		return(c);
	}
};

一般に、演算子のオーバーロードの場合、その部分を .operator+(a)のように置き換えることができます。 たとえば、上の定義のあと、


complex aa = complex(1,-2);
complex bb = aa + 10;

とあると、bbの右辺はaa.operator+(10)と置き換えることができます。 すなわち、


complex aa = complex(1,-2);
complex bb = aa.operator+(10);


とも書けます。 しかし、もし、これが bb = 10 + aa; だと、 10.operator+(aa)となり、整数(int型)値10のクラスの演算子を探そうとしてコンパイルエラーとなりますので注意してください。

一方、上で+と*のオーバーロードを1引数の関数として示しました。 場合によっては、


complex operator+(complex a, complex b);

のように、2引数で、演算子のオーバーロードは書けないだろうか? と思うかもしれません。 しかし、そもそもクラス内の関数として定義すると、 クラス自体のオブジェクトに加えて、 aとbの2つのオブジェクトがパラメータとして入ってきますので、 呼び出し側で2つのオブジェクトを持たねばならず、 クラス内に、 二項演算子の演算子オーバーローディングとして定義することはできません。 それでも、定義したい場合は、 クラスの外で 定義することはできます。 2引数の+演算に関するオーバーローディングは、たとえば以下のように定義して使います。 これと1引数の+演算子のオーバーローディングを混在して使うことは出来ませんので注意してください。 反復になりますが、通常は、二項演算では、1引数の演算子のオーバーローディングで十分です。 ただし、double型+complex型->complex型のような演算は、 2引数の演算子のオーバーローディングでないと実現できません。 このように特殊事情がある場合には、クラスの外に定義する関数を使うことがあります。
complex.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* プログラム5-6 複素数クラス例*/
#ifndef _MYCOMPLEX
#define _MYCOMPLEX
#include <iostream>
using namespace std;

class complex {
	double re, im;/* 実部と虚部 */
public:
	complex(): re(0), im(0) {}/* コンストラクタ */
	complex(double r): re(r), im(0){}/* コンストラクタ */
	complex(double r, double i): re(r), im(i){}/* コンストラクタ */
	complex& operator += (complex b);/* 加算代入 */
	complex& operator += (double b);/* 加算代入 */
	void print();/* プリント */
	double real(){ return re;}/* 実数パートを返す */
	double imag(){ return im;}/* 虚数パートを返す */
};
/* 2引数の演算子のオーバーロードはクラスの外で定義 */
complex operator +(complex a, complex b);/* complex + complex */
complex operator +(complex a, double b);/* complex + double */
complex operator +(double  a, complex b);/* double + complex */
#endif
上で演算子のオーバーローディングでcomplexのあとに、&(青色)があるのは、これらの実体側でthisオブジェクトを変更する場合に必要になります。別のcomplex変数を定義して演算を実現し、thisオブジェクトは読み出ししかしない場合は、必要ありません。
complex.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* プログラム5-7 複素数クラスのメンバー関数例*/
#include "complex.h"

complex&
complex::operator += (complex a){/* complexとの加算 */
	re += a.re;
	im += a.im;
	return *this;
}

complex&
complex::operator += (double a){/* doubleとの加算 */
	re += a;
	return *this;
}

void 
complex::print(){ cout << "(" << re << "," << im << "i)" << endl;}

/* 以下、クラス外関数 */
complex operator +(complex a, complex b){/* complex + complex */
	return a += b;
}
complex operator +(complex a, double b){/* complex + double */
	return {a.real()+b, a.imag()};
}
complex operator +(double  a, complex b){/* double + complex */
	return {a+b.real(), b.imag()};
}
complexMain.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
/* プログラム5-8 引数=2の+演算子の定義例とmain関数例 */
#include "complex.h"

int main(){
    complex x {1,-1}; x.print();
    complex y {-2,4}; y.print();
    auto r1 = x + y;/* complex + complex */
    auto r2 = x + 2;/* complex + double */
    auto r3 = 2 + x;/* double + complex */
    auto r4 = 2 + 3;/* r4のデータ型はint型となる */
    r1.print();
    r2.print();
    r3.print();
    cout << "r4 = " << r4 << endl;
    return 0;
}
上のプログラムでauto型変数を用いていますが、型を自動的に決めてもらう場合に使います。 この例では、実際は最初の3つがcomplex型で、最後のautoはint型となります。 実行結果は以下のようです。



別の例で演算子のオーバーロードを示します。 ここでは、N次元空間のベクトルをクラスで表現し、演算子のオーバーロードの例を示します。
VectorN.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* プログラム5-9 N次元ベクトルを演算子のオーバーロードで定義*/
#include <iostream>
using namespace std;
#include <cmath>
#include <vector>

class VectorN {
  int dim; /* 次元 */
  vector <double> v;/* 要素保持 */
public:
  VectorN (int dim): dim(dim) { v.resize(dim); }
  VectorN (vector <double> v) : v(v) { dim = v.size();}
  /* 以下、演算子のオーバーロード */
  double &operator [ ] (int n) {return v[n];};/* n番目の要素 */
  void operator = (const VectorN &b) ;/* 代入 */
  VectorN operator + (const VectorN &b);/* 和 */
  VectorN operator - (const VectorN &b);/* 差 */
  double operator *(const VectorN &b);/* 内積 */
  double operator !();/* ノルム */
  void print();/* プリント */
};
赤色の部分が演算子のオーバーロードで定義されている部分です。 一見関数の定義のようにはじめりますが、operatorという 予約がある点で明確に異なります。 その直後にくるのが演算子で、( )括弧内が演算の対象となるデータ型を表します。 一般に演算結果の型をTとしたとき、

T operator (xxxx);

のように表現できます。 ここでは、配列演算子以外はメンバー関数のプロトタイプ だけとなっており、その実体は、以下のVectorN.cppファイル内で定義しています。
VectorN.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* プログラム5-10: N次元ベクトルのメンバー関数 */
#include "VectorN.h"

void 
VectorN::operator = (const VectorN &b) {/* 代入 */
	for (int i = 0 ; i < dim; i++)
		this->v[i] = b.v[i];
}

VectorN 
VectorN::operator + (const VectorN &b){/* 和 */
	VectorN a = *this;
	VectorN c(dim);
	for (int i = 0 ; i < dim; i++)
		c.v[i] = a.v[i] + b.v[i];
	return(c);
}

VectorN 
VectorN::operator - (const VectorN &b){/* 差 */
	VectorN a = *this;
	VectorN c(dim);
	for (int i = 0 ; i < dim; i++)
		c.v[i] = a.v[i] - b.v[i];
	return(c);
}

double 
VectorN::operator *(const VectorN &b){/* 内積 */
	VectorN a = *this;
	double t = 0.0;
	for (int i = 0 ; i < dim; i++)
		t += a.v[i] * b.v[i];
	return(t);
}

double 
VectorN::operator !(){/* ノルム */
	VectorN a = *this;
	double t = 0.0;
	for (int i = 0 ; i < dim; i++)
		t += a.v[i] * a.v[i];
	return(sqrt(t));
}

void 
VectorN::print(){/* プリント */
	cout << "[";
	for (int i = 0 ; i < dim-1 ; i++)
		cout << v[i] << "," ;
	cout << v[dim-1] << "]" << endl;
}

最後に、演算子のオーバーロードのあるVectorNクラスを使った以下のmain関数を考えます。
VectorNMain.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* プログラム5-11 VectorNクラスを用いたmain関数の例 */
#include "VectorN.h"
#include <cstdlib>
#include <ctime>
const double SCALE = 20.0;
double myRand(){/* 乱数用ユーティリティ関数 */
	double t = (double)rand()/(double)RAND_MAX;
	return(t);
}
int main(){
	srand(time(nullptr));//現在時刻で乱数を初期化
	vector <double> a;
	vector <double> b;
	cout << "ベクトルの次元数を入力してください > " ;
	int dim=0;
	cin >> dim;
	a.resize(dim); b.resize(dim);
	for (int i = 0 ; i < dim ; i++){
		a[i] = ::SCALE * (::myRand()-0.5);
		b[i] = ::SCALE * (::myRand()-0.5);
	}
	VectorN va(a);//VectorNクラスの変数vaをaで初期化
	VectorN vb(b);//VectorNクラスの変数vbをbで初期化
	cout << "【ベクトルa】" << endl;
	va.print();
	cout << "【ベクトルaのノルム】" << endl;
	cout << !va << endl;
	cout << "【ベクトルb】" << endl;
	vb.print();
	cout << "【ベクトルbのノルム】" << endl;
	cout << !vb << endl;
	VectorN vc = va + vb;
	cout << "【加算演算結果】" << endl;
	vc.print();
	double t = va * vb;
	cout << "【内積演算結果】= " << t << endl;
	return 0;
}
実行結果は以下の通りです。



別の例として、プログラム3-5で示した行列クラスに演算子のオーバーロードを加え、 少し修正したクラス(定義のみ)を示します。
myMatrix.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* プログラム5-12 行列を演算子のオーバーロードで定義した例*/
#include <iostream>
using namespace std;
#include <vector>

class Matrix {
	int row, col;/* 行と列のサイズ */
	vector <vector <double> > v;/* データはポインタのポインタ */
public:
	Matrix(int a, int b);/* コンストラクタ1 */
	Matrix();/* コンストラクタ2 */
	Matrix(const Matrix &);/* コピーコンストラクタ */
	void print(void);/* プリント関数:プロトタイプ */
	int row(void) {return row;};/* 行サイズを返す */
	int column(void) {return col;};/* 列サイズを返す */
	/* 以下、演算子のオーバーロード */
	vector <double>& operator [ ] (int n);/* 配列演算子:n番目の要素 */
	Matrix   operator * (const Matrix &);/* 行列同士の乗算 */
	Matrix   operator + (const Matrix &);/* 行列同士の和 */
	Matrix   operator - (const Matrix &);/* 行列同士の差 */
	void     operator = (const Matrix &);/* 行列の代入 */
};


テンプレートの基礎

オーバーロード(多重定義)、仮想関数によるオーバーライドを含むクラスの継承は、ポリモルフィズム(多形態) というオブジェクト指向言語プログラミングの重要な概念です。 また、ジェネリック・プログラミング の考え方は、多くのオブジェクト指向言語に取り入れられています。 C++では、ジェネリック・プログラミングとして、テンプレート機能が備わっています。 簡単な例を用いて説明すると、 たとえば、あるデータあるソーティング を適用したいとします。 あるデータの候補には、

が候補とします。 オブジェクト指向言語の多くには、このような場合、 ジェネリック・プログラミングのための仕組みが用意されています。

C++では、 テンプレート を用いてジェネリック・プログラミングが可能です。 たとえば、

template <typename T> void func(..){ }

のように宣言して、テンプレート型Tを作成することで実現できます。 関数func()の型はvoidに限りません。

また、関数レベルでなく、 クラスに対してテンプレートを定義することもしばしばあり、 クラステンプレートと呼ばれることがあります。 実際は、これらを組み合わせて、クラスレベルならびに、 クラス内の関数レベルでテンプレートを定義して利用します。 たとえば、

template <typename T> class myClass { };

のように宣言してクラスに対するテンプレートを実現できる仕組みがあります。 実例で示しましょう。

データ型のテンプレート例

以下は、これまでも何度か登場したSwap関数の引数のデータをテンプレートで ジェネリック・プログラム化した例です。
SwapTemplate.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* プログラム5-13 Swap関数のデータ型をテンプレートで一般化するジェネリック・プログラミング*/
#include <iostream>
using namespace std;

template <typename T>
void Swap(T * a, T * b) /* ポインタでパラメータを渡す */
{
	T temp = *b;
	*b = *a;
	*a = temp;
}

int main(){
	string hello = "world!", world = "Hello, ";
	cout << "Swap前 " << hello << world << endl; /* 出力は"Hello, world!" */
	Swap( &world, &hello );/* 文字列としてSwap */
	cout << "Swap後 " << hello << world << endl; /* 出力は"Hello, world!" */
	double x = 3.0, y = -2.0;
	cout << "Swap前 x = " << x << " y = " << y << endl;
	Swap( &y, &x );/* double型としてSwap */
	cout << "Swap後 x = " << x << " y = " << y << endl;
	return 0;
}

クラスのテンプレート例(線形リスト+図形クラス)

クラスのテンプレートの例として、いろいろなデータ型を線形リストで連結することを考えます。 ここでは、前回示した2次元図形でクラスを継承する例をそのまま使い、 Shape2Dクラスのデータとする線形リストをテンプレートで作る例を紹介します。
myList.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* プログラム5-14: myList.h 線形リストクラス定義(テンプレート版) */
#ifndef MY_LIST_TEMPLATE
#define MY_LIST_TEMPLATE
#include <iostream>
using namespace std;

template <typename T>
class myList {/* 線形リストクラス */
  private: /* 外からはこれらのデータに直接アクセスさせない */
	T* data;/* 線形リストのデータ */
	class myList *next;/* nextリンク */
  public:/* 以降はpublicで公開する関数(メソッド) */
	myList(){}/* コンストラクタ */
	myList ( T* data ): data(data){}/* コンストラクタ:多重定義 */
	myList *insert ( T* data ){/*リストの末尾に挿入 */
		this->next = new myList<T>( data );
		return(this->next);
	}
	T* getData (){return data;}
	void setData (T* data ){ this->data = data; }
	myList *getNext(){return next;}
	void deleteList(myList *head){/* 明示的に呼び出す、再帰的な線形リスト削除 */
		if (head != nullptr){
			deleteList(head->next);
			delete head;
		}
	}
};
#endif /* myList.h */

main関数では、以下のようにして上述のテンプレート版の線形リストクラスを利用できます。
myListMain.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* プログラム5-15: myListMain.cpp 線形リストクラス定義 */
#include "myList.h"
#include "Shape2D.h"
#include "Square.h"
#include "Circle.h"
#include <ctime>
#include <cstdlib>
const float XRANGE = 550.0;
const float YRANGE = 650.0;
const float RGBRANGE = 1.0;
double myRand(){return(rand()/(double)RAND_MAX);}/* [0-1]の乱数を発生 */

int main(){
	int N = 3;/* 3個の要素をリストに挿入する */
	myList <Shape2D> *head = nullptr;/* 線形リストの先頭要素 */
	myList <Shape2D> *list = nullptr;/* リストクラスへのポインタ */
	srand ( time(nullptr) );/* 現在時刻で乱数を初期化 */
	double x0, y0, length;
	double cx, cy, r;
	Shape2D * shape;
	/* 最初の3個は正方形 */
	for ( int i = 0 ; i < N ; i++ ){
		x0 = ::XRANGE * ::myRand();
		y0 = ::YRANGE * ::myRand();
		length = ::XRANGE * ::myRand();
		shape = new Square(x0, y0, length);
		if (i == 0) head = list = new myList<Shape2D>(shape);
		else list = list->insert(shape);
	}
	/* 後半の3個は円 */
	for ( int i = 0 ; i < N ; i++ ){
		cx = ::XRANGE * ::myRand();
		cy = ::YRANGE * ::myRand();
		r = ::XRANGE * ::myRand();
		shape = new Circle(cx, cy, r);
		list = list->insert(shape);
	}
	/* 線形リストをプリント */
	myList<Shape2D> *p = head;/* 線形リストの先頭からたどる */
	int count = 1;
	while (p != nullptr){
		shape = p->getData();/* 線形リストのデータを取出す */
		cout << count << "番目の図形" << endl;
		shape->print();/* 派生クラスに応じてprint関数呼び出し */
		count++;
		p = p->getNext();/* nextはprivateなので公開された関数でアクセス */
	}
	head->deleteList(head);	/* 線形リストの明示的な削除 */
	return 0;
}
派生クラスのポインタは線形リストに加えていないことに注意してください。 つまり、基底クラスのポインタだけ線形リストに加えれば、インスタンスの種類に応じて print関数を呼び出したときに、適宜その派生クラスに対応する関数を呼び出してくれます。 実行結果は以下のようです。




クラスのテンプレート例(クイックソート+被ソートデータ)

以下は、クイックソートに関するクラスのテンプレート例です。
myQuickSort.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* プログラム5-16: myQuickSort.h クイックソートのためのクラス */
#ifndef _MYQUICKSORT
#define _MYQUICKSORT

#include <iostream>
using namespace std;
#include <vector>

template <typename T>
class myQuickSort {
	vector <T> A; /* データ領域 */
 private:
 	void swap( int a, int b);/* indexによるデータの交換 */
	void partition( int &L, int &R);/* 分割 */
 public:
 	myQuickSort(){}
 	myQuickSort(vector <T> &A): A(A) {}/* コンストラクタ */
 	~myQuickSort(){}
 	void sort(int L, int R);/* クイックソート本体 */
	void print();
};
#include "myQuickSort.cpp"/* コンパイル時には合体が必要 */
#endif
myQuickSort.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* プログラム5-17: myQuickSort.cpp クイックソートのためのクラス内メンバー変数*/
template <typename T>
void myQuickSort<T>::swap(int i, int j){/* indexによるデータの交換 */
	T tmp;
	tmp = A[i];
	A[i] = A[j];
	A[j] = tmp;
}

template <typename T>
void myQuickSort<T>::partition( int &L, int &R){
    int m = (L+R)>>1;	/* ピボットを選択 */
    T x = A[m];
    do {
        while (A[L] < x) L++;/* 左からスキャン */
        while (A[R] > x) R--;/* 右からスキャン */
        if ( L <= R ){/* 交換 */
            swap(L, R);
            L++;  R--;
        }
    } while ( L <= R );
}

template <typename T>
void myQuickSort<T>::sort(int left, int right){/* divide-and-conquer(分割統治) */
	int L = left, R = right;
	partition(L, R);/* 分割 */
	if (left < R)  sort(left, R);/* 左半分再帰的に呼び出す */
	if (L < right) sort(L, right);/* 右半分再帰的に呼び出す */
}

template <typename T>
void myQuickSort<T>::print(){/* ソート結果のプリント */
	int size = A.size();
	for (int i = 0 ; i < size ; i++){
		cout << A[i] << " ";
	}
	cout << endl;
}
myQuickSortMain.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* プログラム5-18: myQuickSortMain.cpp クイックソートクラスの利用例 */
#include "myQuickSort.h"

int main(){
    vector <int> int_data = {2,8,7,1,3,5,6,4};/* 整数データ */
    vector <float> float_data = {3.4, -2.9, 11.4, 3.0, 7.5, -10.0};/* 実数データ */
    /* データをプリント */
    cout << "整数データのクイックソート例" << endl;
    myQuickSort<int> *myQSI = new myQuickSort<int>(int_data);
    myQSI->print();
    myQSI->sort(0,int_data.size()-1);
    myQSI->print();
    delete myQSI;
    cout << "実数データのクイックソート例" << endl;
    myQuickSort<float> *myQSF = new myQuickSort<float>(float_data);
    myQSF->print();
    myQSF->sort(0,float_data.size()-1);
    myQSF->print();
    delete myQSF;
    return 0;
}
実行結果は以下のようです。なお、ここでは、myQuickSort.cppは、 myQuickSort.hの中から呼び出しているため、分割コンパイルの必要はありません。