こんにちは、ナナです。
『関数のオーバーロード』に続き、次は「演算子のオーバーロード」です。
演算子のオーバーロードって言われても、イメージが付きづらいかもしれません。
実はC++のクラスにおいて、「+」「-」を代表とする演算子たちは関数として扱うことが可能なのです。
つまり、クラス毎にこれらの演算子を関数として定義し、クラス独自の処理を行わせることができるのです。
それでは「演算子のオーバーロード」の使い方を解説していきましょう。
演算子のオーバーロードがもたらす便利さとは

押忍!「演算子を関数として扱う」って、何を言っているのか全然わかんないっす。どういうことなんすかっ?自分にもわかるように説明してほしいっす!

そうだね、最初は意味がわからないよね。でもね、見た目は演算子なんだけど実際は関数として動いているってことなんですよ。
C言語の構造体は「+」「ー」はできない
C言語で座標を管理する「S_POS」構造体を定義し、P1(100、200)とP2(300、400)という2つの座標があったとします。
もし、「P3 = P1+P2」を行えたとしたら、P3(400、600)という答えを期待したいですよね。
// 座標管理構造体
typedef struct
{
int x;
int y;
} S_POS;
int main()
{
S_POS pos1 = { 100, 200 }; // 座標1
S_POS pos2 = { 300, 400 }; // 座標2
S_POS pos3;
// 座標1と座標2を足すことはできない!
pos3 = pos1 + pos2;
return 0;
}
しかし、座標変数「pos1」「pos2」を足した結果が欲しくても、「+」演算子を直接使うことはできません。
C言語では、各X・Y要素を個別に足して、新たな「pos3」変数に設定するしかないのです。
// X・Y要素を個別に足すしかない!
pos3.x = pos1.x + pos2.x;
pos3.y = pos1.y + pos2.y;

座標構造体に対する「足し算」なんてものは、私たちは「X」「Y」をそれぞれ足せばいいと判断できますが、コンパイラは何が正しいのかわからないのです。
C++において、この演算を可能にするのが「演算子のオーバーロード」です。
POS座標クラスで「+演算子」をオーバーロードしてみよう
それでは「+演算子」をPOSクラスでオーバーロードで定義してみましょう。白抜きの部分が対象となります。
#ifndef POS_H
#define POS_H
class POS
{
public:
int x;
int y;
POS();
void setPos(int x, int y);
POS operator+(POS rhs);
};
#endif // POS_H
#include "POS.h"
POS::POS()
{
this->x = 0;
this->y = 0;
}
void POS::setPos(int x, int y)
{
this->x = x;
this->y = y;
}
POS POS::operator+(POS rhs)
{
POS pos;
pos.x = this->x + rhs.x;
pos.y = this->y + rhs.y;
return pos;
}
クラス側の下準備ができたら、main.cppで実際に使ってみましょう。
#include <stdio.h>
#include "POS.h"
int main()
{
POS pos1; // 座標1
POS pos2; // 座標2
POS pos3;
pos1.setPos(100, 200);
pos2.setPos(300, 400);
// 「+」演算子のオーバーロードで実施可能
pos3 = pos1 + pos2;
printf("x:%d y:%d", pos3.x, pos3.y);
return 0;
}
x:400 y:600
このようにクラスオブジェクト同士の足し算によって、求めていた結果が得られるようになりました。

POSクラス同士の足し算をしたときの処理内容をメンバ関数として定義しておくことで、「+」という演算子の動作をエンジニアが制御できるようになったのです。
演算子のオーバーロードの定義方法

押っ忍!演算子のオーバーロードはどんな風に定義すればいいっすか?書き方って決まってるんすか?

演算子のオーバーロードは、「operator」というキーワードを使って定義します。書き方を解説しようね。
「operator」を使った演算子のオーバーロード定義方法
演算子のオーバーロードには特別な書き方が必要となります。基本的な書き方は次のようになります。

「operator」という名前に「演算子」を組み合わせて書くことで、演算子のオーバーロードと認識されます。
注意点として、「引数」と「戻り値」は演算子の種類や、何を作用させたいかによって変化することです。
例えば先ほどの座標の足し算において、「POSオブジェクト + 整数」の組み合わせで足し算したい場合は、引数に整数型を持つ関数を用意します。
#include <stdio.h>
#include "POS.h"
int main()
{
POS pos1; // 座標1
POS pos2; // 座標2
pos1.setPos(100, 200);
// 「+」演算子で整数値を足す
pos2 = pos1 + 50;
printf("x:%d y:%d", pos2.x, pos2.y);
return 0;
}
POS POS::operator+(int value)
{
POS pos;
pos.x = this->x + value;
pos.y = this->y + value;
return pos;
}
このように、演算子の種類や何を作用させたいかによって、引数や戻り値のデータ型を変化させる必要があるため、皆さん自身が最適な形を見極めて定義しなければなりません。

C++は本当に様々なことができる言語です。そのためエンジニアはそれを操る技術が求められます。この辺りがC++の難しさですね。
演算子のオーバーロードの別の呼び出し方
オーバーロードされた演算子は「operator」を使った関数呼び出しの形式でも呼び出すことができます。
次の2つのプログラムは全く同じ結果となります。
#include <stdio.h>
#include "POS.h"
int main()
{
POS pos1; // 座標1
POS pos2; // 座標2
POS pos3;
pos1.setPos(100, 200);
pos2.setPos(300, 400);
// 「+」演算子を使った呼び出し
pos3 = pos1 + pos2;
printf("x:%d y:%d", pos3.x, pos3.y);
return 0;
}
#include <stdio.h>
#include "POS.h"
int main()
{
POS pos1; // 座標1
POS pos2; // 座標2
POS pos3;
pos1.setPos(100, 200);
pos2.setPos(300, 400);
// メンバ関数を使った呼び出し方
pos3 = pos1.operator+(pos2);
printf("x:%d y:%d", pos3.x, pos3.y);
return 0;
}
これこそが「演算子を関数として扱う」という正体です。
私たちの目には「+」の演算子を利用しているように見えますが、C++側では関数呼び出しの形で解釈しているということなのです。

左側に書かれたオブジェクトが主になり、右側が引数として与えられるこの構成が、演算子のオーバーロードの基本となります。
本例の+演算子のオーバーロードの呼び出しでは、「pos1」がthisポインタとなり「pos2」が引数として与えられることになります。

関数として展開された姿をイメージできると、オーバーロード関数の引数や戻り値の形がしっくりきますよね。
Q&A:演算子のオーバーロードに関するよくある質問
Q:演算子のオーバーロードを定義するのはどんなとき?

押忍っ!演算子のオーバーロードって結局は関数なんすよね?普通に関数を定義して呼び出したらいいじゃないっすか?
なんで演算子をわざわざ関数にして使う必要があるっすか?

確かに関数呼び出しでしかないのですから、座標同士の足し算であれば「addPos」のようなメンバ関数を作って対処することもできますね。
しかし、「C++」という言語はプログラムの作り手である皆さんに対して、クラスがより自然な姿として見えるようなコーディング方法を提供しているのです。
それでは、演算子のオーバーロードと、座標の足し算を行う「addPos」メンバ関数を定義した場合で、呼び出し側のプログラムにどのような違いが出るかを考察してみましょう。
pos3 = pos1 + pos2;
pos3 = pos1.addPos(pos2);
どうでしょうか?演算子のオーバーロードを使った方が直感的で見やすくありませんか?
このように演算子という記号は、読み手に対して自然な解釈をもたらしてくれる効果があります。
Q:演算子のオーバーロードを定義するときの注意点は?

押忍っ!「演算子」って普段あんまり意識してなかったっすけど、この記号によって自然な解釈ができるようになってるんすね。気づけたことに感激っす。
じゃあ、演算子のオーバーロードを定義するときに注意すべきことってあるんすか?

その通り、演算子のオーバーロードは「自然な解釈ができること」が大事なんだよ。
つまり、使う側にとって直感的にイメージできない演算子のオーバーロードは控えるべきだということだね!
C言語やC++にある数多くの演算子は、その記号そのものが持つイメージがあります。演算子のオーバーロードを行う時は、このイメージを崩さない範囲で定義することが大事です。
例えば、座標同士の足し算はイメージできそうですが、割り算って言われると困りますよね。

このように、その演算子によってもたらされる効果が、使う側にとってイメージできないと返って混乱を招いてしまいます。

演算子のオーバーロードを定義する時は、皆さん自身が使う側の意識に立って、その演算子の効果を自然に受け入れられるかを判断基準にするとよいでしょう。

C++のカリキュラムを順に学びたい方はこちらからどうぞ~