記事Overloading C ++ operatorを読んだ後、この記事を書きたいという要望が現れました。なぜなら、多くの重要なトピックがこの記事でカバーされていなかったからです。
覚えておくべき最も重要なことは、演算子のオーバーロードです。これは、関数を呼び出すためのより便利な方法であるため、演算子のオーバーロードに関与しないでください。 コードを簡単に記述できる場合にのみ使用してください。 しかし、それはそれほど読むことを難しくしませんでした。 結局のところ、コードは書かれているよりもずっと頻繁に読み取られます。 また、組み込み型と並行して演算子をオーバーロードすることは決して許可されないことを忘れないでください。オーバーロードの可能性はカスタム型/クラスのみです。
オーバーロード構文
演算子のオーバーロード構文は、演算子@という名前の関数の定義に非常に似ています。@は演算子の識別子です(例:+、-、<<、>>)。 最も単純な例を考えてみましょう。
class Integer { private: int value; public: Integer(int i): value(i) {} const Integer operator+(const Integer& rv) const { return (value + rv.value); } };
この場合、演算子はクラスのメンバーとしてフレーム化され、引数は演算子の右側にある値を決定します。 一般に、演算子をオーバーロードするには、主に2つの方法があります。クラスに優しいグローバル関数、またはクラス自体のインライン関数です。 どのメソッド、どの演算子がより良いかについては、トピックの最後で検討します。
ほとんどの場合、演算子(条件付き演算子を除く)は、オブジェクト、または引数が関係する型への参照を返します(型が異なる場合は、演算子の評価結果の解釈方法を決定します)。
単項演算子のオーバーロード
上記で定義されたIntegerクラスの単項演算子のオーバーロードの例を見てみましょう。 同時に、それらをわかりやすい関数として定義し、デクリメント演算子とインクリメント演算子を検討します。
class Integer { private: int value; public: Integer(int i): value(i) {} // + friend const Integer& operator+(const Integer& i); // - friend const Integer operator-(const Integer& i); // friend const Integer& operator++(Integer& i); // friend const Integer operator++(Integer& i, int); // friend const Integer& operator--(Integer& i); // friend const Integer operator--(Integer& i, int); }; // . const Integer& operator+(const Integer& i) { return i.value; } const Integer operator-(const Integer& i) { return Integer(-i.value); } // const Integer& operator++(Integer& i) { i.value++; return i; } // const Integer operator++(Integer& i, int) { Integer oldValue(i.value); i.value++; return oldValue; } // const Integer& operator--(Integer& i) { i.value--; return i; } // const Integer operator--(Integer& i, int) { Integer oldValue(i.value); i.value--; return oldValue; }
これで、コンパイラーが前置バージョンと後置バージョンのデクリメントとインクリメントを区別する方法がわかりました。 彼が式++ iを見た場合、演算子++(a)関数が呼び出されます。 彼がi ++を見つけた場合、演算子++(a、int)が呼び出されます。 つまり、オーバーロードされた演算子++関数が呼び出され、このために、postfixバージョンでダミーのintパラメーターが使用されます。
二項演算子
二項演算子をオーバーロードする構文を検討してください。 l値を返す1つのステートメント、1つの条件付きステートメント、および新しい値を作成する1つのステートメントをオーバーロードします(グローバルに定義します)。
class Integer { private: int value; public: Integer(int i): value(i) {} friend const Integer operator+(const Integer& left, const Integer& right); friend Integer& operator+=(Integer& left, const Integer& right); friend bool operator==(const Integer& left, const Integer& right); }; const Integer operator+(const Integer& left, const Integer& right) { return Integer(left.value + right.value); } Integer& operator+=(Integer& left, const Integer& right) { left.value += right.value; return left; } bool operator==(const Integer& left, const Integer& right) { return left.value == right.value; }
これらのすべての例で、演算子は1つのタイプに対してオーバーロードされていますが、これは必須ではありません。 たとえば、その類似度で定義されているInteger型とFloat型の追加をオーバーロードできます。
引数と戻り値
ご覧のとおり、この例では、関数に引数を渡し、演算子の値を返すために異なるメソッドを使用しています。
- 引数が演算子によって変更されない場合、たとえば単項プラスの場合、定数への参照として渡す必要があります。 一般的に、これはほとんどすべての算術演算子(加算、減算、乗算...)に当てはまります
- 戻り値のタイプは、ステートメントの性質によって異なります。 演算子が新しい値を返す必要がある場合は、新しいオブジェクトを作成する必要があります(バイナリプラスの場合)。 オブジェクトを左辺値として変更することを禁止する場合は、定数を返す必要があります。
- 代入演算子の場合、変更されたアイテムへのリンクを返す必要があります。 また、(x = y).f()という形式の構成で代入演算子を使用する場合、変数xに対してf()関数を呼び出し、yに代入した後、定数への参照を返さず、参照のみを返します。
- ブール演算子は、最悪のintおよび最高のboolを返す必要があります。
戻り値の最適化
新しいオブジェクトを作成して関数から返す場合、上記のバイナリプラス演算子の例のようにレコードを使用します。
return Integer(left.value + right.value);
正直なところ、C ++ 11に関連する状況はわかりませんが、以下のすべての引数はC ++ 98に有効です。
一見、これは一時オブジェクトを作成するための構文に似ています。つまり、上記のコードと次のコードの間に違いがないかのようです。
Integer temp(left.value + right.value); return temp;
しかし実際には、この場合、コンストラクターは最初の行で呼び出され、次にコピーコンストラクターが呼び出され、オブジェクトがコピーされます。次に、スタックが巻き戻されると、デストラクターが呼び出されます。 最初のレコードを使用する場合、コンパイラは最初にオブジェクトをメモリ内に作成してコピーするため、コピーコンストラクターとデストラクターへの呼び出しを保存します。
特別なオペレーター
C ++には、特定の構文とオーバーロードメソッドを持つ演算子があります。 たとえば、インデックス演算子[]。 常にクラスのメンバーとして定義され、配列としてのインデックス付きオブジェクトの動作が暗示されるため、参照を返す必要があります。
コンマ演算子
「特別な」演算子には、カンマ演算子も含まれます。 横にカンマが付いているオブジェクトに対して呼び出されます(ただし、関数の引数リストでは呼び出されません)。 この演算子を使用する意味のある例を考え出すことはそれほど簡単ではありません。 過負荷に関する前の記事へのコメントのHabrauser AxisPodは、 1つの こと について述べました 。
ポインター逆参照演算子
これらの演算子をオーバーロードすることは、スマートポインタークラスに対して正当化される場合があります。 この演算子は必然的にクラスの関数として定義され、いくつかの制限が課せられます。オブジェクト(またはリンク)またはオブジェクトにアクセスできるポインターを返す必要があります。
代入演算子
代入演算子は、クラスの関数として必ず定義されます。これは、「=」の左側のオブジェクトと密接にリンクしているためです。 代入演算子をグローバルに定義すると、「=」演算子の標準動作をオーバーライドできます。 例:
class Integer { private: int value; public: Integer(int i): value(i) {} Integer& operator=(const Integer& right) { // if (this == &right) { return *this; } value = right.value; return *this; } };
ご覧のとおり、機能の開始時に、自己割り当てのチェックが行われます。 一般に、この場合、自己割り当ては無害ですが、状況は必ずしもそれほど単純ではありません。 たとえば、オブジェクトが大きい場合、不必要なコピーやポインターの操作に多くの時間を費やすことができます。
非オーバーロード演算子
C ++の一部の演算子は、原則としてオーバーロードされていません。 どうやら、これはセキュリティ上の理由で行われます。
- クラスメンバーを選択する演算子は「。」です。
- クラス「。*」のメンバーへのポインターを逆参照する演算子
- C ++では、(Fortranのような)指数演算子「**」はありません。
- 独自の演算子を定義することは禁止されています(優先順位付けに問題がある可能性があります)。
- オペレーターの優先順位は変更できません
オペレーター定義フォームのガイドライン
すでにわかったように、演算子には2つの方法があります。クラス関数の形式と、使いやすいグローバル関数の形式です。
Rob Murrayは、著書C ++ Strategies and Tacticsで 、演算子形式を選択するための以下のガイドラインを概説しました。
オペレーター
| 推奨フォーム
|
すべての単項演算子
| クラスのメンバー
|
=()[]->-> *
| 必須のクラスメンバー
|
+ =-= / = * = ^ =&= | =%= >> = << =
| クラスのメンバー
|
残りの二項演算子
| クラスのメンバーではない
|
なぜそう 第一に、一部のオペレーターは当初制限されていました。 一般に、演算子の定義方法に意味的な違いがない場合、接続を強調するクラス関数として設計することをお勧めします。さらに、関数はインラインになります。 また、左側のオペランドを別のクラスのオブジェクトとして表す必要がある場合があります。 おそらく最も印象的な例は、入力/出力ストリームの<<および>>の再定義です。
文学
ブルース・エッケル-C ++の哲学。 標準C ++の紹介 。