クラス補足

【未完】

this ポインタとアロー演算子

外に関数の定義を書く事

動的確保

無名クラスの変数の宣言

ビットフィールド

メンバへのポインタ

広義クラス

【未完】

構造体

共用体

無名クラス

列挙体

演算子のオーバーロード

茲では、一つ一つの演算子のオーバーロードの仕方について説明を行っています。 一般的な演算子のオーバーロードの説明に関しては、本編の方を御覧下さい。

単項演算子のオーバーロード

単項演算子は、それぞれ下の様に定義します。二項演算子の適用先は自分自身 (this) になります。

class ClassA{
	...

public:
	〈戻〉 operator+() const{ ... } // 正号
	〈戻〉 operator-() const{ ... } // 負号
	〈戻〉 operator!() const{ ... } // 否定
	〈戻〉 operator~() const{ ... } // bitwise not
	〈戻〉 operator*() const{ ... } // 間接参照
	〈戻〉 operator&() const{ ... } // アドレス取得
	ClassA& operator++(){
		...
		return *this;
	} // 前置インクリメント
	ClassA& operator--(){
		...
		return *this;
	} // 前置デクリメント
	ClassA operator++(int){ ... } // 後置インクリメント
	ClassA operator--(int){ ... } // 後置デクリメント
};

特に ++/-- の前置・後置の区別については説明が必要であると思います。 ++/-- の前置・後置の区別はダミーの引数で行います。 無引数の operator++/operator-- は前置演算子として動作します。 引数として int 一つを取る物は後置演算子として動作します。 この時、引数の int はダミーであって何の意味も持ちませんし何の値が入っているのか分かりませんので、 わざわざ引数としての名前を与える必要はなく (int) と型だけの指定で構いません。

前置演算子だけが定義されて後置演算子が定義されていない場合に、 後置演算子を使用しようとすると、前置演算子で代用されることがあります。 この時、前置演算子と後置演算子で異なる動作結果を齎す場合に予期せぬ動作を生む可能性があるので、 前置演算子を定義する時には後置演算子も一緒に定義する様にしましょう。

また、グローバルな場所に記述する場合には、以下の様にします。 メンバとして定義していた時に this として受け取っていた物を、 引数として明示的に受け取ります。 それ以外の点 (戻り値の型や引数の選び方) は、メンバとして記述した場合と同じです。

class ClassB{
	...
	
	friend 〈戻〉 operator+(const ClassB& r);
	friend 〈戻〉 operator-(const ClassB& r);
	friend 〈戻〉 operator!(const ClassB& r);
	friend 〈戻〉 operator~(const ClassB& r);
	friend 〈戻〉 operator*(const ClassB& r);
	friend 〈戻〉 operator&(const ClassB& r);
	friend ClassB& operator++(ClassB& r);
	friend ClassB& operator--(ClassB& r);
	friend ClassB operator++(ClassB& r,int);
	friend ClassB operator--(ClassB& r,int);
};
〈戻〉 operator+(const ClassB& r){ ... }
〈戻〉 operator-(const ClassB& r){ ... }
〈戻〉 operator!(const ClassB& r){ ... }
〈戻〉 operator~(const ClassB& r){ ... }
〈戻〉 operator*(const ClassB& r){ ... }
〈戻〉 operator&(const ClassB& r){ ... }
ClassB& operator++(ClassB& r){ ... }
ClassB& operator--(ClassB& r){ ... }
ClassB operator++(ClassB& r,int){ ... }
ClassB operator--(ClassB& r,int){ ... }

二項演算子のオーバーロード

二項演算子は、左右に二つの引数を取る演算子のことです。 茲では、左の引数の事を左オペランドと呼び、右の引数のことを右オペランドと呼ぶことにします。

定義

二項演算子をメンバとして定義する場合は以下の様にします。 自分自身 (this) が左オペランドで、第一引数に右オペランドが渡されます。

class ClassA{
	...
	
public:
	〈戻〉 operator+(〈引〉r) const{ ... }
	〈戻〉 operator-(〈引〉r) const{ ... }
	〈戻〉 operator*(〈引〉r) const{ ... }
	〈戻〉 operator/(〈引〉r) const{ ... }
	〈戻〉 operator%(〈引〉r) const{ ... }
	〈戻〉 operator&(〈引〉r) const{ ... }
	〈戻〉 operator|(〈引〉r) const{ ... }
	〈戻〉 operator^(〈引〉r) const{ ... }
	〈戻〉 operator<<(〈引〉r) const{ ... }
	〈戻〉 operator>>(〈引〉r) const{ ... }
	〈戻〉 operator&&(〈引〉r) const{ ... }
	〈戻〉 operator||(〈引〉r) const{ ... }
	bool operator==(〈引〉r) const{ ... }
	bool operator!=(〈引〉r) const{ ... }
	bool operator>(〈引〉r) const{ ... }
	bool operator<(〈引〉r) const{ ... }
	bool operator>=(〈引〉r) const{ ... }
	bool operator<=(〈引〉r) const{ ... }
};

グローバルな場所に実装する場合には、以下の様に定義します。 グローバルな場所に定義すると、右オペランドに自分で作った型を貰って、左オペランドに別の型のインスタンスを貰うと言うことが可能になります。

class ClassB{
	...

	friend 〈戻〉 operator+(〈左〉 l,〈右〉 r);
	friend 〈戻〉 operator-(〈左〉 l,〈右〉 r);
	friend 〈戻〉 operator*(〈左〉 l,〈右〉 r);
	friend 〈戻〉 operator/(〈左〉 l,〈右〉 r);
	friend 〈戻〉 operator%(〈左〉 l,〈右〉 r);
	friend 〈戻〉 operator|(〈左〉 l,〈右〉 r);
	friend 〈戻〉 operator&(〈左〉 l,〈右〉 r);
	friend 〈戻〉 operator^(〈左〉 l,〈右〉 r);
	friend 〈戻〉 operator&&(〈左〉 l,〈右〉 r);
	friend 〈戻〉 operator||(〈左〉 l,〈右〉 r);
	friend 〈戻〉 operator<<(〈左〉 l,〈右〉 r);
	friend 〈戻〉 operator>>(〈左〉 l,〈右〉 r);
	friend bool operator==(〈左〉 l,〈右〉 r);
	friend bool operator!=(〈左〉 l,〈右〉 r);
	friend bool operator<=(〈左〉 l,〈右〉 r);
	friend bool operator>=(〈左〉 l,〈右〉 r);
	friend bool operator<(〈左〉 l,〈右〉 r);
	friend bool operator>(〈左〉 l,〈右〉 r);
	friend bool operator,(〈左〉 l,〈右〉 r);
};
〈戻〉 operator+(〈左〉 l,〈右〉 r){ ... }
〈戻〉 operator-(〈左〉 l,〈右〉 r){ ... }
〈戻〉 operator*(〈左〉 l,〈右〉 r){ ... }
〈戻〉 operator/(〈左〉 l,〈右〉 r){ ... }
〈戻〉 operator%(〈左〉 l,〈右〉 r){ ... }
〈戻〉 operator|(〈左〉 l,〈右〉 r){ ... }
〈戻〉 operator&(〈左〉 l,〈右〉 r){ ... }
〈戻〉 operator^(〈左〉 l,〈右〉 r){ ... }
〈戻〉 operator&&(〈左〉 l,〈右〉 r){ ... }
〈戻〉 operator||(〈左〉 l,〈右〉 r){ ... }
〈戻〉 operator<<(〈左〉 l,〈右〉 r){ ... }
〈戻〉 operator>>(〈左〉 l,〈右〉 r){ ... }
bool operator==(〈左〉 l,〈右〉 r){ ... }
bool operator!=(〈左〉 l,〈右〉 r){ ... }
bool operator<=(〈左〉 l,〈右〉 r){ ... }
bool operator>=(〈左〉 l,〈右〉 r){ ... }
bool operator<(〈左〉 l,〈右〉 r){ ... }
bool operator>(〈左〉 l,〈右〉 r){ ... }
bool operator,(〈左〉 l,〈右〉 r){ ... }

実装の目安

使いやすい (== 何も考えずに書いてもバグを生まない) 演算子を作成する為には、色々な注意が必要です。 出来るだけ使い手から見て自然になる様に定義することで、そのクラスについて殊更に勉強しなくても使いやすい様にクラスを設計することが出来るのです。 (とは言っても、必ずしもその注意に従わなければならないわけではなく、新しい演算子の使用法を提唱しても構いません。 が、無闇にそんな事をしても大抵その使い方に段々嫌気がさして来て失敗になります…)

  1. + * / & | ^ && || の演算子では可換性を満たす様に設計する様にして下さい。 特に、左右のオペランドの型が異なる物を定義した場合には、引数の左右を交換した物も定義する様にして下さい。
  2. operator+/operator* については結合性を満たす様に設計します。 つまり (a+b)+c==a+(b+c) 等を満たす様にすると言うことです。 また、可能ならば分配則を満たす様に設計すると尚良いでしょう。 とは言っても、実際には結合性を満たさない様な集合も考えることが出来るので、これはそれ程強い要求ではありません。
  3. できれば、- 及び / を + 及び * の厳密な逆演算になる様に定義すると良いでしょう。
  4. == は可換 (つまり反射律) を満たす様にしましょう。
  5. == < > <= >= は推移律を満たす様にしましょう。

それぞれの演算子についての、細かい点に関しては以降も参考にして下さい。

比較演算子

また、順序に関する演算子ついては互いに色々な関係が成り立つ様に設計します。

これを満たす様に設計するの一つの方法は、operator<= だけ真面目に設計して他は全て operator<= で構成するという方法です。 operator<= の推移性だけに留意しておけば問題ありません。

class ClassC{
public:
	bool operator<=(const ClassC& r) const{ /* 真面目に実装 */ }
	bool operator>=(const ClassC& r) const{return r<=*this;}
	bool operator<(const ClassC& r) const{return !(r<=*this);}
	bool operator>(const ClassC& r) const{return !(*this<=r);}
	bool operator==(const ClassC& r) const{return *this<=r&&*this>=r;}
	bool operator!=(const ClassC& r) const{return *this<r||r<*this;}
};

もっと見通しの良い方法としては、比較関数を一つ定義しておいてそれを使ってそれぞれの演算子を構成するという物があります。 これも、比較関数に於ける推移性だけに留意しておけば問題は生じない筈です。

class ClassC{
public:
	bool operator<=(const ClassC& r) const{return compare(*this,r)<=0;}
	bool operator>=(const ClassC& r) const{return compare(*this,r)>=0;}
	bool operator<(const ClassC& r) const{return compare(*this,r)<0;}
	bool operator>(const ClassC& r) const{return compare(*this,r)>0;}
	bool operator==(const ClassC& r) const{return compare(*this,r)==0;}
	bool operator!=(const ClassC& r) const{return compare(*this,r)!=0;}
private:
	static int compare(const ClassC& l,const ClassC& r){
		// l より r の方が先に来る場合に負の数を返す
		// l より r の方が後に来る場合に正の数を返す
		// l と r が等しい場合に 0 を返す
	}
};

論理和・論理積

|| 演算子や && 演算子の場合、オーバーロードせずに使うと、 左オペランドを評価した時点で結果が確定した場合、右オペランドの評価は行われません。

然し、オーバーロードした場合には単なる関数呼び出しに置き換えられるので、 演算子を呼び出す前に左のオペランドも右のオペランドも評価してから実行される事になります。 つまり、短絡評価が無効になるのです。

この事はクラスの使い手に混乱を与える可能性があるので、 無闇に || や && はオーバーロードしない様にしましょう。

カンマ演算子

, 演算子についてもオーバーロードが可能ですが、 「これも唯式を並べて書いただけのつもりなのに、良く分からない計算が為されてしまった」 という事に為りかねませんので、オーバーロードしない様にするのが身の為です。

若し、どうしてもオーバーロードしたい時には、左オペランドをカンマ演算子以外で使わない様な型にすると良いと思います。 (つまり、カンマ演算子を使う為だけに新しい型を定義して、それ以外の用途に使わない様にするという事です。)

ストリーム演算子

「ストリーム演算子」と言ってもそう言う名前の演算子があるわけではありません。 ここでは、シフト演算子を iostream に対して使用する場合について述べたいので、適当にそう言う見出しにしただけです。

シフト演算子をストリームに対して使うというと…以下の様なコードです。

#include <iostream>
int main(){
	int i;
	std::cin>>i;
	std::cout<<i<<std::endl;
	return EXIT_SUCCESS;
}

int 型に対してこの様なことが出来るのであれば、 自分で定義したクラスについても、同じ様なことをしたくなります。 その為には、<< 演算子を自分で定義しなければなりません。 然し、その方法は知らないと少々戸惑ってしまうかも知れないので、特に茲で紹介しようと思います。

<< 演算子の左オペランドは自分で作ったクラスではなくストリームでなければならないので、 演算子はグローバルに定義しなければ為りません。更にストリームには文字型によって色々あります。 結局下の様に実装しなければ為りません。

class Complex{
public:
	double real,imag;

	template<typename T>
	friend std::basic_ostream<T>& operator<<(std::basic_ostream<T>& o,const Complex& v);
};
template<typename T>
std::basic_ostream<T>& operator<<(std::basic_ostream<T>& o,const Complex& v){
	/* 茲に内容を出力するコードを記述します。 */
	
	// ↓ これは唯の例なので、imag が負である可能性や、
	// real または imag が 0 である可能性などを考慮していません。
	return o<<v.real<<" + "<<v.imag<<"i";
}

代入演算子のオーバーロード

代入演算子は勿論、二項演算子の一種です。 然し、代入演算子のオーバーロードは少し他の演算子と異なる所があるので特別に説明する事にします。

先ず、代入演算子に関しては自分で定義しなくても使用する事が出来ます。 = 演算子であれば、単純なインスタンス内容のコピーとして扱われます。 また、+= 等の複合演算子に関しては [a+=b を a=a+b と解釈する] という形で扱われます。 (但し、この場合には + 演算子が定義されている必要があります。 また一方で、+= 演算子だけ定義して、+ 演算子を定義しないといった事も可能です。 この場合には += の演算は使えても + の演算は使用できないことになります。)

代入演算子は、左辺は必ず自分の定義したクラスインスタンスでなければならない為、メンバ関数としてしか定義出来ません。 定義する際には、以下の様にします。

class ClassA{
public:
	ClassA& operator=(const ClassA& r){ ... return *this;}
	ClassA& operator+=(const ClassA& r){ ... return *this;}
	ClassA& operator-=(const ClassA& r){ ... return *this;}
	ClassA& operator*=(const ClassA& r){ ... return *this;}
	ClassA& operator/=(const ClassA& r){ ... return *this;}
	ClassA& operator%=(const ClassA& r){ ... return *this;}
	ClassA& operator^=(const ClassA& r){ ... return *this;}
	ClassA& operator|=(const ClassA& r){ ... return *this;}
	ClassA& operator&=(const ClassA& r){ ... return *this;}
	ClassA& operator<<=(const ClassA& r){ ... return *this;}
	ClassA& operator>>=(const ClassA& r){ ... return *this;}
};

代入演算子の戻り値の型や右オペランドの型は自由ですが、通常は上の様にします。 更に、戻り値としては自分自身の参照を返す様に設計するのが普通です。

また、当然の事ながら動作はその演算子にあった物にする必要があります。 例えば、単純な代入演算子ならば、処理が終わった後には自分自身の内容は右オペランドの内容と同じ様な内容になっていなければ為りません。 また、a+=b 等の複合の演算子については、a=a+b と同じ結果が得られる様にしなければ為りません。

よくある実装

よくある実装としては += 演算子を定義して、それを元に + 演算子を定義する (或いは両演算子を独立に定義する) 等といった方法があります。 これは、勿論 += 演算子に限った話ではなくて /= や ^= 等他の物についても同様です。

class Complex{
	...
	
	// インスタンスの生成を伴わない
	Complex& operator+=(const Complex& r){
		this->real+=r.real;
		this->imag+=r.imag;
		return *this;
	}
	Complex operator+(const Complex& r) const{
		Complex ret=*this;
		return ret+=r;
	}
};

もし、+= 演算子を定義せずに + 演算子と = 演算子の組合せでコンパイルされる様にしていた場合、 + 演算の際に無駄にインスタンスを生成してしまう事になります。 然し、+= 演算子を自分で定義する様にしておけば、無駄なインスタンスの生成を防ぐ事が可能になり、 より効率の良いプログラムを作成する事ができます。

添字演算子のオーバーロード

【未完】

関数呼び出し演算子のオーバーロード

【未完】

メンバアクセス演算子のオーバーロード

【未完】

メモリ割り当て演算子のオーバーロード

【未完】


起稿 2008-11-10
© 2008-2009, K. Murase myoga.murase@gmail.com
inserted by FC2 system