注:回答は特定の順序で与えられていましたが、多くのユーザーは回答が与えられた時間ではなく、投票数に応じて回答を分類しているため、ここでは最も意味のある順序で回答の索引を示しています。
(注)これはStack Overflow's C++ FAQへのエントリーを意味しています。もし、このような形でFAQを提供するアイデアを批判したいのであれば、この件の発端となったmetaへの投稿がその場所となるでしょう。この質問に対する回答は、FAQのアイデアが最初に始まったC++ chatroomで監視されていますので、あなたの回答はアイデアを出した人に読まれる可能性が非常に高いです)。
演算子のオーバーロードで行われる作業のほとんどは定型的なコードです。演算子は単なる構文上の記号であり、実際の作業は単純な関数で行うことができる(そしてしばしば関数に転送される)ので、それも不思議ではありません。しかし、この定型的なコードを正しく理解することは重要です。もし失敗すると、演算子のコードがコンパイルされなかったり、ユーザーのコードがコンパイルされなかったり、ユーザーのコードが驚くような動作をしたりします。
代入については、いろいろと言いたいことがあります。しかし、そのほとんどはGManの有名なCopy-And-Swap FAQですでに語られているので、ここではほとんど省略して、完璧な代入演算子だけを参考のために挙げておきます。
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
ビットシフト演算子 <<
と >>
は、C言語から継承したビット操作機能としてハードウェアインターフェースではまだ使用されていますが、ほとんどのアプリケーションでは、オーバーロードされたストリーム入出力演算子として普及しています。 ビット操作演算子としてのオーバーロードについては、後述の「バイナリ演算子」の項を参照してください。 iostreamsでオブジェクトを使用する際に、独自のカスタムフォーマットや解析ロジックを実装する方法については、続きをご覧ください。
ストリーム演算子は、オーバーロードされることの多い演算子の中でも、メンバーであるか非メンバーであるかについて、構文上の制限がないバイナリ infix 演算子です。
ストリーム演算子は、左引数を変更する(ストリームの状態を変更する)ため、経験則によれば、左オペランドの型のメンバーとして実装されるべきです。しかし、左オペランドは標準ライブラリのストリームであり、標準ライブラリで定義されているストリームの出力・入力演算子のほとんどは、確かにストリームクラスのメンバとして定義されていますが、自分の型に出力・入力演算を実装する場合、標準ライブラリのストリームの型を変更することはできません。そのため、自分の型のためのこれらの演算子を非メンバー関数として実装する必要があります。
この2つの正統な形式は次のとおりです。
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// read obj from stream
if( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
operator>>` を実装する際に、手動でストリームの状態を設定する必要があるのは、読み込み自体は成功したものの、結果が期待したものと異なる場合のみです。
ファンクタとして知られる関数オブジェクトを作成するために使用される関数呼び出し演算子は、__member___関数として定義されなければならないため、常にメンバ関数の暗黙の this
引数を持ちます。これ以外にも、ゼロを含む任意の数の追加引数を取るようにオーバーロードすることができます。
以下に構文の例を示します。
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
使用例です。
foo f;
int a = f("hello");
C++の標準ライブラリでは、関数オブジェクトは常にコピーされます。そのため、自分の関数オブジェクトはコピーされにくいものでなければなりません。関数オブジェクトがどうしてもコピーコストのかかるデータを使用する必要がある場合は、そのデータを別の場所に保存し、関数オブジェクトがそれを参照するようにしたほうがよいでしょう。
バイナリ infix の比較演算子は、経験則上、非メンバ関数1として実装すべきです。単項接頭辞の否定 !
は、(同じルールに基づいて)メンバー関数として実装すべきです。(しかし、それをオーバーロードするのは通常良い考えではありません。)
標準ライブラリのアルゴリズム(例:std::sort()
)や型(例:std::map
)は、常にoperator<
が存在することだけを期待します。したがって、operator<
を定義する場合は、演算子オーバーロードの3つ目の基本ルールに従い、他のすべてのブーリアン比較演算子も定義するようにしてください。これらを実装する標準的な方法は以下の通りです。
inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}
ここで重要なことは、これらの演算子のうち実際に何かを行うのは2つだけで、他の演算子は実際の作業を行うために引数をこの2つのどちらかに転送しているだけだということです。
残りのバイナリブーリアン演算子(||
, &&
)をオーバーロードする構文は、比較演算子のルールに従います。しかし、これら2の妥当な使用例を見つけることは非常に難しいでしょう。
1 他の経験則と同様に、時にはこれを破る理由があるかもしれません。その場合、メンバ関数では *this
となるバイナリ比較演算子の左側のオペランドも const
である必要があることを忘れないでください。つまり、メンバ関数として実装された比較演算子は、次のようなシグネチャを持つ必要があります。
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(最後の const
に注意してください。)
2 組み込みバージョンの ||
や &&
はショートカットセマンティクスを使用していることに注意してください。一方、ユーザー定義のものは(メソッド呼び出しのための構文上の糖であるため)ショートカット・セマンティクスを使用しません。ユーザーはこれらの演算子がショートカットセマンティクスを持つことを期待し、 コードがそれに依存する可能性があるため、これらの演算子を定義しないことを強くお勧めします。
##算術演算子### 単項の算術演算子###。 単項のインクリメントおよびデクリメント演算子には、プレフィックス型とポストフィックス型があります。前置詞と後置詞を区別するために、後置詞の場合はさらにダミーの int 引数を取ります。incrementやdecrementをオーバーロードする場合は、必ず前置と後置の両方を実装するようにしてください。 以下は increment の正規の実装で、decrement も同じ規則に従います。
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
postfixはprefixと同じように実装されていることに注意してください。また、postfix は余分なコピーを行うことにも注意してください 2。
単項の minus や plus をオーバーロードすることはあまり一般的ではなく、おそらく避けたほうがいいでしょう。必要であれば、これらはおそらくメンバ関数としてオーバーロードされるべきです。
2 また、後置修飾語は前置修飾語よりも多くの仕事をするため、使用効率が悪いことにも注意してください。これは、一般的に postfix の増分よりも prefix の増分を好む良い理由です。コンパイラは通常、ビルトイン型に対してはポストフィックスインクリメントの追加作業を最適化することができますが、ユーザー定義型(リストのイテレータのような無害なもの)に対しては同じことができないかもしれません。i++に慣れてしまうと、
iが組み込み型でない場合に
++iを覚えておくのが大変になるので (さらに、型を変更するときにはコードを変更しなければなりません)、明示的に postfix が必要な場合を除いて、常に prefix インクリメントを使用するようにしたほうがよいでしょう。 </sub> ###2進数の算術演算子###。 2進法の演算子では、第3の基本ルールである演算子のオーバーロードを忘れてはいけません。演算子のオーバーロード:
+を提供するなら
+=も提供する、
-を提供するなら
-=を省略しない、など。Andrew Koenigは、複合代入演算子が非複合代入演算子のベースとして使用できることを最初に観察した人物と言われています。つまり、演算子
+は
+=で実装され、
-は
-=で実装される、ということです。 我々の経験則によれば、
+とその仲間はノンメンバーであるべきで、左の引数を変更する複合代入の対応物(
+=など)はメンバーであるべきです。以下に
+=と
+` の例示的なコードを示します。他の二項演算子も同じように実装してください。
class X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs)
{
lhs += rhs;
return lhs;
}
演算子+=はその結果を参照ごとに返し、
演算子+はその結果のコピーを返します。もちろん、通常はコピーを返すよりも参照を返す方が効率的ですが、
operator+の場合はコピーを回避する方法がありません。a + b
と書いたとき、結果が新しい値になることを期待しているので、operator+
は新しい値を返さなければならないのです。3
また、operator+
は左オペランドをconstリファレンスではなく、__by copy___で受け取ることに注意してください。この理由は、operator=
がコピーごとに引数を取る理由を述べたのと同じです。
ビット操作の演算子 ~
&
|
^
<<
>
は、算術演算子と同じように実装する必要があります。しかし、(出力や入力に対して <<
と >>
をオーバーロードすることを除いて)これらをオーバーロードする合理的な使用例はほとんどありません。
3 ここから得られる教訓は、一般的に a += b
は a + b
よりも効率的であり、可能であればそちらを優先すべきであるということです。
配列の添え字演算子は、クラスのメンバーとして実装しなければならない二項演算子です。この演算子は、データ要素にキーでアクセスできるコンテナのような型に使用します。 これらを提供する標準的な形式は次のとおりです。
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
クラスのユーザが operator[]
で返されるデータ要素を変更できないようにしたい場合を除き(その場合は non-const 型を省略できます)、常に演算子の両方の型を提供する必要があります。
value_type が組み込み型を参照していることがわかっている場合、演算子の const バリアントは const 参照ではなくコピーを返す方が良いでしょう。
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
独自のイテレータやスマートポインタを定義するには、単項の前置参照解消演算子 *
と二項の infix ポインタメンバアクセス演算子 ->
をオーバーロードする必要があります。
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
->
では、value_type
が class
(または struct
や union
) 型の場合、別の operator->()
が再帰的に呼び出され、operator->()
がクラス型でない値を返すまで繰り返されます。
単項の address-of 演算子は決してオーバーロードしてはいけません。
operator->*()`についてはこの質問を参照してください。めったに使われないので、オーバーロードされることはほとんどありません。実際、イテレータでもオーバーロードしません。変換演算子](https://stackoverflow.com/questions/4421706/operator-overloading/16615725#16615725)へ続く。
C++での演算子オーバーロードには、従うべき__3つの基本ルールがあります。このようなルールには、例外もあります。時にはルールを逸脱して悪いコードになってしまった人もいますが、そのような積極的な逸脱はほとんどありません。少なくとも、私が見たそのような逸脱行為の100件中99件は正当なものではありませんでした。しかし、1000のうち999だったとしてもおかしくはありません。だから、次のようなルールを守った方がいいでしょう。
1.演算子の意味が明らかに明確でない場合は、オーバーロードしてはいけません。 基本的に、演算子のオーバーロードに関する第一のルールは、その根底にこうあります。Don't do it」です。演算子のオーバーロードについては多くのことが知られており、多くの記事や本の章、その他のテキストがこれらすべてを扱っているので、奇妙に思えるかもしれません。しかし、この一見明白な証拠にもかかわらず、演算子のオーバーロードが適切なケースは驚くほど少ないのです。その理由は、応用分野での演算子の使い方がよく知られており、議論の余地がない限り、実際には演算子の適用の背後にあるセマンティクスを理解することは難しいからです。一般的な考えに反して、このようなケースはほとんどありません。
1.演算子のよく知られたセマンティクスに常にこだわること。
C++では、オーバーロードされた演算子のセマンティクスに制限はありません。C++はオーバーロードされた演算子のセマンティクスに制限を設けていません。コンパイラは、右オペランドから減算するバイナリの+
演算子を実装したコードを喜んで受け入れます。しかし、このような演算子のユーザーは、a + b
という式がa
からb
を引くものだとは決して思わないでしょう。もちろん、これはアプリケーション・ドメインにおける演算子のセマンティクスが議論の余地のないものであることを前提としています。
1.演算子は互いに関連している。
演算子はお互いに、そして他の演算子に関連しています。もしあなたの型が a + b
をサポートしているなら、ユーザーは a += b
も呼び出せることを期待するでしょう。もしその型がプレフィックスインクリメント ++a
をサポートしていれば、ユーザーは a++
も動作することを期待するでしょう。a < bをチェックできれば、
a > b` もチェックできることを期待するでしょう。もし彼らがあなたの型をコピーコンストラクトすることができれば、代入も同様に動作することを期待します。
会員と非会員の判断](https://stackoverflow.com/questions/4421706/operator-overloading-in-c/4421729#4421729)に続きます。
C++では、組み込み型に対する演算子の意味を変えることはできません。演算子は、ユーザー定義型1に対してのみオーバーロードすることができます。つまり、オペランドの少なくとも 1 つはユーザー定義型でなければなりません。他のオーバーロードされた関数と同様に、演算子は特定のパラメータのセットに対して一度だけオーバーロードすることができます。
C++では、すべての演算子をオーバーロードできるわけではありません。オーバーロードできない演算子には次のようなものがある。.`::
sizeof
typeid
.*
や、C++で唯一の三項演算子である ?:
などがあります。
C++でオーバーロード可能な演算子には次のようなものがあります。
+
-
*
/
%
および +=
-=
*=
/=
%=
(すべて2進数のインフィックス)、 +
-
(単項のプレフィックス)、++
--
(単項のプレフィックスおよびポストフィックス)。&
|
^
<<
>
および &=
|=
^=
<<=
>=
(すべてのバイナリインフィクス); ~
(単項接頭辞)==
!=
<
>
<=
>=
||
&&
(すべてのバイナリインフィクス); !
(単数形の接頭辞)new
new[]
`delete
delete[]
=
[]
->
->*
,
(すべてのバイナリインフィックス); *
&
(すべてのユニアルプレフィックス) ()
(関数呼び出し、n-aryインフィックス)ただし、これらすべてをオーバーロードすることができるからといって、オーバーロードすべきであるとは限りません。演算子オーバーロードの基本ルールをご覧ください。
C++では、演算子は特別な名前を持つ関数の形でオーバーロードされます。他の関数と同様に、オーバーロードされた演算子は一般に、左オペランドの型のメンバ関数として、または非メンバ関数__として実装することができます。2 オブジェクト x に適用される単項演算子 @
3 は、 operator@(x)
または x.operator@()
として呼び出されます。オブジェクト x
と y
に適用される2項の infix 演算子 @
は、 operator@(x,y)
または x.operator@(y)
として呼び出されます。4
メンバーではない関数として実装されている演算子は、オペランドの型と友達になることがあります。
1 「ユーザー定義」という言葉は、少し誤解を招くかもしれません。C++では、組み込み型とユーザー定義型を区別しています。前者はint、char、doubleなどに属し、後者は標準ライブラリのものも含め、すべてのstruct、class、union、enum型に属します。
2 これについては、このFAQの後編で説明します。
3 @
はC++では有効な演算子ではないので、プレースホルダーとして使用しています。
4 C++の唯一の三項演算子はオーバーロードできず、唯一のn項演算子は常にメンバ関数として実装されなければなりません。
C++における演算子オーバーロードの3つの基本ルール】(https://stackoverflow.com/questions/4421706/operator-overloading-in-c/4421708#4421708)に続きます。