オーバーロードする一般的な演算子
演算子をオーバーロードする作業のほとんどは定型コードです。演算子は単なるシンタックス シュガーであるため、実際の作業は単純な関数で実行できます (多くの場合、関数に転送されます)。しかし、この定型コードを正しく理解することが重要です。失敗すると、オペレーターのコードがコンパイルされないか、ユーザーのコードがコンパイルされないか、ユーザーのコードが驚くべき動作をするかのいずれかになります。
代入演算子
課題については、言いたいことがたくさんあります。ただし、そのほとんどはGMan の有名な Copy-And-Swap FAQですでに述べられているため、ここではそのほとんどを省略し、参照用に完全な代入演算子のみをリストします。
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Bitshift 演算子 (ストリーム I/O に使用)
ビットシフト演算子<<
および>>
は、C から継承したビット操作関数のハードウェア インターフェースでまだ使用されていますが、ほとんどのアプリケーションでオーバーロードされたストリーム入力および出力演算子としてより普及しています。ビット操作演算子としてのオーバーロードのガイダンスについては、以下の二項算術演算子に関するセクションを参照してください。オブジェクトが iostream で使用される場合に独自のカスタム形式と解析ロジックを実装するには、続行します。
最も一般的にオーバーロードされる演算子の 1 つであるストリーム演算子は、メンバーか非メンバーかについて構文で制限が指定されていない二項中置演算子です。それらは左の引数を変更する (ストリームの状態を変更する) ため、経験則に従って、左のオペランドの型のメンバーとして実装する必要があります。ただし、それらの左側のオペランドは標準ライブラリからのストリームであり、標準ライブラリによって定義されたストリーム出力および入力演算子のほとんどは実際にはストリーム クラスのメンバーとして定義されていますが、独自の型の出力および入力操作を実装する場合は、標準ライブラリのストリーム タイプを変更することはできません。そのため、独自の型に対してこれらの演算子を非メンバー関数として実装する必要があります。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>>
、ストリームの状態を手動で設定する必要があるのは、読み取り自体が成功した場合のみですが、結果は期待したものではありません。
関数呼び出し演算子
関数オブジェクト (ファンクターとも呼ばれる) の作成に使用される関数呼び出し演算子は、メンバー関数として定義する必要があるため、常にthis
メンバー関数の暗黙の引数を持ちます。これ以外に、オーバーロードして、ゼロを含む任意の数の追加引数を取ることができます。
構文の例を次に示します。
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
使用法:
foo f;
int a = f("hello");
C++ 標準ライブラリ全体で、関数オブジェクトは常にコピーされます。したがって、独自の関数オブジェクトは安価にコピーする必要があります。関数オブジェクトが絶対にコピーにコストのかかるデータを使用する必要がある場合は、そのデータを別の場所に保存し、関数オブジェクトに参照させる方がよいでしょう。
比較演算子
2 項中置比較演算子は、経験則に従って、非メンバー関数として実装する必要があります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組み込みバージョンの andは、ショートカット セマンティクスを使用することに注意してください。ユーザー定義のものは (メソッド呼び出しのシンタックス シュガーであるため)、ショートカット セマンティクスを使用しません。ユーザーは、これらの演算子がショートカットのセマンティクスを持っていることを期待しており、そのコードはそれに依存している可能性があります。したがって、絶対に定義しないことを強くお勧めします。||
&&
算術演算子
単項算術演算子
単項インクリメントおよびデクリメント演算子には、前置および後置の両方のフレーバーがあります。それぞれを見分けるために、後置バリアントは追加のダミーの int 引数を取ります。インクリメントまたはデクリメントをオーバーロードする場合は、必ず前置バージョンと後置バージョンの両方を実装してください。これはインクリメントの標準的な実装であり、デクリメントは同じ規則に従います。
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
接尾辞バリアントは、接頭辞に関して実装されていることに注意してください。また、postfix は余分なコピーを行うことに注意してください。2
単項マイナスとプラスのオーバーロードはあまり一般的ではなく、おそらく避けるのが最善です。必要に応じて、メンバー関数としてオーバーロードする必要があります。
2 また、後置バリアントはより多くの作業を行うため、前置バリアントよりも使用効率が低いことに注意してください。これは、通常、後置インクリメントよりも前置インクリメントを優先する正当な理由です。コンパイラは通常、組み込み型の後置インクリメントの追加作業を最適化できますが、ユーザー定義型 (リスト反復子のように無邪気に見えるもの) に対しては同じことができない場合があります。に慣れると、 が組み込み型でない場合に代わりにi++
行うことを覚えるのが非常に難しくなります(さらに、型を変更するときはコードを変更する必要があります)。後置が明示的に必要でない限り、前置増分を使用します。++i
i
二項算術演算子
+
2 項算術演算子については、演算子のオーバーロードに関する第 3 の基本規則に従うこと+=
を忘れないで-
ください-=
。演算子は、対応する非複合演算子のベースとして使用できます。つまり、演算子+
は で実装され+=
、-
は で実装されます-=
。
私たちの経験則によれば、+
そのコンパニオンは非メンバーである必要がありますが、複合代入の対応するもの (+=
など) は、左の引数を変更してメンバーである必要があります。+=
とのコード例を次に+
示します。他の二項算術演算子は、同じ方法で実装する必要があります。
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+=
は参照ごとに結果をoperator+
返しますが、結果のコピーを返します。もちろん、通常は参照を返す方がコピーを返すよりも効率的ですが、 の場合、operator+
コピーを回避する方法はありません。を書くときa + b
、結果が新しい値であることを期待します。そのため、新しい値operator+
を返さなければなりません。3また、 const 参照ではなくコピーによってoperator+
左オペランドを取得する
ことにも注意してください。この理由は、コピーごとに引数を取る理由と同じです。operator=
ビット操作演算子~
&
|
^
<<
>>
は、算術演算子と同じ方法で実装する必要があります。ただし、(オーバーロード<<
と>>
出力と入力を除いて) これらをオーバーロードする合理的な使用例はほとんどありません。
3 繰り返しになりますが、このことから得られる教訓a += b
は、一般に、より効率的でa + b
あり、可能であれば優先されるべきであるということです。
配列添字
配列添え字演算子は、クラス メンバーとして実装する必要がある二項演算子です。キーによるデータ要素へのアクセスを許可するコンテナのような型に使用されます。これらを提供する標準的な形式は次のとおりです。
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
クラスのユーザーが によって返されるデータ要素を変更できないようにしたくないoperator[]
場合 (その場合、非 const バリアントを省略できます) を除き、演算子の両方のバリアントを常に提供する必要があります。
value_type が組み込み型を参照することがわかっている場合、演算子の const バリアントは、const 参照ではなくコピーを返す必要があります。
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
ポインターのような型の演算子
独自の反復子またはスマート ポインターを定義するには、単項プレフィックス逆参照演算子*
とバイナリ インフィックス ポインター メンバー アクセス演算子をオーバーロードする必要があり->
ます。
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
これらも、ほとんどの場合、const バージョンと非 const バージョンの両方が必要になることに注意してください。->
演算子の場合、value_type
がclass
(orstruct
またはunion
) 型の場合、 が非クラス型の値を返すまで、別の演算子operator->()
が再帰的に呼び出されます。operator->()
単項アドレス取得演算子はオーバーロードしないでください。
この質問operator->*()
を参照してください。めったに使用されないため、過負荷になることはめったにありません。実際、イテレータでさえオーバーロードしません。
変換演算子に進む