2338

注: 回答は特定の順序で提供されていますが、多くのユーザーは、回答が提供された時間ではなく、投票に従って回答を並べ替えるため、最も意味のある順序で回答のインデックスを次に示します。

(注: これはStack Overflow の C++ FAQへのエントリであることを意図しています。このフォームで FAQ を提供するという考えを批判したい場合は、すべての始まりとなった meta への投稿がそれを行う場所になります。回答への回答その質問は、FAQ のアイデアが最初に始まったC++ チャットルームで監視されているため、アイデアを思いついた人にあなたの回答が読まれる可能性が非常に高くなります。)

4

7 に答える 7

1139

オーバーロードする一般的な演算子

演算子をオーバーロードする作業のほとんどは定型コードです。演算子は単なるシンタックス シュガーであるため、実際の作業は単純な関数で実行できます (多くの場合、関数に転送されます)。しかし、この定型コードを正しく理解することが重要です。失敗すると、オペレーターのコードがコンパイルされないか、ユーザーのコードがコンパイルされないか、ユーザーのコードが驚くべき動作をするかのいずれかになります。

代入演算子

課題については、言いたいことがたくさんあります。ただし、そのほとんどは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++行うことを覚えるのが非常に難しくなります(さらに、型を変更するときはコードを変更する必要があります)。後置が明示的に必要でない限り、前置増分を使用します。++ii

二項算術演算子

+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_typeclass(orstructまたはunion) 型の場合、 が非クラス型の値を返すまで、別の演算子operator->()が再帰的に呼び出されます。operator->()

単項アドレス取得演算子はオーバーロードしないでください。

この質問operator->*()を参照してください。めったに使用されないため、過負荷になることはめったにありません。実際、イテレータでさえオーバーロードしません。


変換演算子に進む

于 2010-12-12T12:47:05.033 に答える
531

C++ での演算子オーバーロードの 3 つの基本ルール

C++ での演算子のオーバーロードに関しては、従うべき 3 つの基本的なルールがあります。そのようなすべての規則と同様に、実際には例外があります。時々人々はそれらから逸脱し、結果は悪いコードではありませんでしたが、そのような肯定的な逸脱はほとんどありません. 少なくとも、私が見た 100 件の逸脱のうち 99 件は不当なものでした。ただし、1000 点満点中 999 点だったのと同じである可能性もあります。したがって、次のルールを守ってください。

  1. 演算子の意味が明らかに明確でなく、議論の余地がない場合はいつでも、それをオーバーロードするべきではありません。 代わりに、適切に選択された名前の関数を提供してください。
    基本的に、演算子をオーバーロードするための最初で最も重要なルールは、その中心にあるもので、次のように述べています。これは奇妙に思えるかもしれません。なぜなら、演算子のオーバーロードについて知るべきことがたくさんあり、多くの記事、本の章、その他のテキストがこれらすべてを扱っているからです。しかし、この一見明白な証拠にもかかわらず、演算子のオーバーロードが適切なケースは驚くほど少ないです。. その理由は、実際には、アプリケーション ドメインでの演算子の使用がよく知られており、議論の余地がない限り、演算子の適用の背後にあるセマンティクスを理解するのは難しいからです。一般に信じられていることとは反対に、これはめったにありません。

  2. 常に、オペレーターの既知のセマンティクスに固執してください。
    C++ は、オーバーロードされた演算子のセマンティクスに制限を課しません。コンパイラは+、右側のオペランドから減算する二項演算子を実装するコードを喜んで受け入れます。ただし、このような演算子のユーザーは、からa + b減算する式を疑うことはありません。もちろん、これは、アプリケーション ドメインでの演算子のセマンティクスが明白であることを前提としています。ab

  3. 関連する一連の操作のすべてを常に提供します。
    演算子は相互に関連しており、他の操作とも関連しています。タイプが をサポートしている場合a + b、ユーザーは も呼び出せることを期待しますa += b。プレフィックスのインクリメント++aをサポートしている場合、同様に機能することが期待a++されます。かどうかを確認できる場合はa < ba > b. 型をコピーして構築できる場合は、代入も同様に機能することを期待しています。


メンバーと非メンバー間の決定に進みます。

于 2010-12-12T12:45:14.160 に答える
279

C++ での演算子のオーバーロードの一般的な構文

C++ の組み込み型の演算子の意味を変更することはできません。演算子はユーザー定義型に対してのみオーバーロードできます1。つまり、少なくとも 1 つのオペランドがユーザー定義型でなければなりません。他のオーバーロードされた関数と同様に、演算子は特定のパラメーター セットに対して 1 回だけオーバーロードできます。

すべての演算子を C++ でオーバーロードできるわけではありません。オーバーロードできない演算子には次. :: sizeof typeid .*のものがあります。C++ で唯一の三項演算子、?:

C++ でオーバーロードできる演算子には次のものがあります。

  • 算術演算子: + - * / %and += -= *= /= %=(すべてのバイナリ インフィックス); + -(単項プレフィックス); ++ --(単項接頭辞と接尾辞)
  • ビット操作: & | ^ << >>and &= |= ^= <<= >>=(すべてバイナリ インフィックス); ~(単項プレフィックス)
  • ブール代数: == != < > <= >= || &&(すべてバイナリ中置); !(単項プレフィックス)
  • メモリ管理:new new[] delete delete[]
  • 暗黙の変換演算子
  • その他: = [] -> ->* , (すべてバイナリ インフィックス); * &(すべて単項プレフィックス) ()(関数呼び出し、n 項インフィックス)

ただし、これらすべてをオーバーロードできるからといって、そうすべきというわけではありません。演算子のオーバーロードの基本ルールを参照してください。

C++ では、演算子は特別な名前を持つ関数の形式でオーバーロードされます。他の関数と同様に、オーバーロードされた演算子は通常、左側のオペランドの型のメンバー関数として、またはメンバー関数として実装できます。いずれかを自由に選択できるか、使用する必要があるかは、いくつかの基準によって異なります。2オブジェクト x に適用される単項演算子@3operator@(x)は、 asまたは as のいずれかで呼び出されますx.operator@()@オブジェクトxandに適用される二項中置演算子は、 asまたは as のyいずれかで呼び出されます。4operator@(x,y)x.operator@(y)

非メンバー関数として実装されている演算子は、オペランドの型のフレンドになる場合があります。

1 「ユーザー定義」という用語は、少し誤解を招く可能性があります。C++ では、組み込み型とユーザー定義型が区別されます。前者には、int、char、double などがあります。後者には、標準ライブラリのものを含むすべての構造体、クラス、共用体、および列挙型が属しますが、それらはユーザーによって定義されていません。

2 これについては、この FAQの後半で説明します。

3 @C++ では有効な演算子ではないため、プレースホルダーとして使用します。

4 C++ の唯一の三項演算子はオーバーロードできず、唯一の n 項演算子は常にメンバー関数として実装する必要があります。


C++ での演算子オーバーロードの 3 つの基本ルールに進みます。

于 2010-12-12T12:46:30.573 に答える
275

会員と非会員の間の決定

二項演算子=(割り当て)、[](配列サブスクリプション)、->(メンバー アクセス)、および n 項()(関数呼び出し) 演算子は、言語の構文で必要とされるため、常にメンバー関数として実装する必要があります。

他の演算子は、メンバーまたは非メンバーとして実装できます。ただし、一部の関数は通常、非メンバー関数として実装する必要があります。これは、左側のオペランドを変更できないためです。これらの中で最も顕著なものは入力演算子と出力演算子<<>>、左側のオペランドは変更できない標準ライブラリのストリーム クラスです。

メンバー関数として実装するか、非メンバー関数として実装するかを選択する必要があるすべての演算子について、次の経験則を使用して決定します。

  1. 単項演算子の場合は、メンバー関数として実装します。
  2. 二項演算子が両方のオペランドを同等に扱う(変更されないままにする) 場合は、この演算子を非メンバー関数として実装します。
  3. 二項演算子が両方のオペランドを同等に扱わない場合 (通常は左側のオペランドが変更されます)、オペランドのプライベート部分にアクセスする必要がある場合は、左側のオペランドの型のメンバー関数にすることが役立つ場合があります。

もちろん、すべての経験則と同様に、例外もあります。タイプがあれば

enum Month {Jan, Feb, ..., Nov, Dec}

インクリメント演算子とデクリメント演算子をオーバーロードしたい場合、これをメンバー関数として行うことはできません。これは、C++ では列挙型にメンバー関数を含めることができないためです。したがって、フリー関数としてオーバーロードする必要があります。またoperator<()、クラス テンプレート内にネストされたクラス テンプレートは、クラス定義でインラインのメンバー関数として実行すると、読み書きがはるかに簡単になります。しかし、これらは確かにまれな例外です。

(ただし、例外を作成する場合constは、メンバー関数の場合は暗黙の引数になるオペランドの -ness の問題を忘れないでくださいthis。非メンバー関数としての演算子がその左端の引数をconst参照として受け取る場合、メンバー関数と同じ演算子は、参照constを行うために末尾にa が必要です。)*thisconst


オーバーロードする一般的な演算子に進みます。

于 2010-12-12T12:49:13.380 に答える
158

オーバーロードnewdelete演算子

注:これはオーバーロードされた演算子の実装ではなく、オーバーロードandの構文のみを扱います。オーバーロードのセマンティクスは独自の FAQ に値する思いますが、演算子のオーバーロードのトピック内では、私はそれを正当化することはできません。newdeletenewdelete

基本

C++ では、次のような新しい式new T(arg)を記述すると、この式が評価されるときに 2 つのことが起こります。最初operator newに が呼び出されて生のメモリが取得され、次に適切なコンストラクタTが呼び出されて、この生のメモリが有効なオブジェクトに変換されます。同様に、オブジェクトを削除すると、まずそのデストラクタが呼び出され、次にメモリが に戻されoperator deleteます。
C++ では、メモリ管理と、割り当てられたメモリでのオブジェクトの構築/破棄の両方の操作を調整できます。後者は、クラスのコンストラクタとデストラクタを記述することによって行われます。メモリ管理を微調整するには、独自のoperator newandを記述しますoperator delete

演算子のオーバーロードの基本ルールの最初の -しないでください- は、特にnewand のオーバーロードに適用されdeleteます。これらの演算子を過負荷にするほとんどの理由は、パフォーマンスの問題メモリの制約です。多くの場合、使用されるアルゴリズムの変更などの他のアクションは、メモリ管理を微調整するよりもはるかに高いコスト/ゲイン比を提供します。

C++ 標準ライブラリには、事前定義されたnewanddelete演算子のセットが付属しています。最も重要なものは次のとおりです。

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

最初の 2 つはオブジェクトのメモリの割り当て/割り当て解除、後の 2 つはオブジェクトの配列のメモリの割り当て/割り当て解除です。これらの独自のバージョンを提供すると、それらはオーバーロードされませんが、標準ライブラリのものを置き換えます。
をオーバーロードする場合は、呼び出すつもりがなくてもoperator new、一致する も常にオーバーロードする必要があります。operator deleteその理由は、新しい式の評価中にコンストラクターがスローした場合、ランタイム システムは、オブジェクトを作成するためのメモリを割り当てるために呼び出されたoperator delete一致するものにメモリを返すためです。 、デフォルトのものが呼び出されますが、これはほとんどの場合間違っています。と をオーバーロードする場合は、配列バリアントもオーバーロードすることを検討する必要があります。operator newoperator delete
newdelete

配置new

C++ では、new および delete 演算子が追加の引数を取ることができます。
いわゆる新しい配置により、特定のアドレスにオブジェクトを作成できます。これは次の場所に渡されます。

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

標準ライブラリには、このための new および delete 演算子の適切なオーバーロードが付属しています。

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

上記のプレースメント new のサンプル コードでoperator deleteは、X のコンストラクターが例外をスローしない限り、決して呼び出されないことに注意してください。

newanddeleteを他の引数でオーバーロードすることもできます。プレースメント new の追加の引数と同様に、これらの引数もキーワードの後の括弧内にリストされますnew。単に歴史的な理由から、そのようなバリアントは、引数が特定のアドレスにオブジェクトを配置するためのものではない場合でも、新しい配置と呼ばれることがよくあります。

クラス固有の新規および削除

最も一般的なのは、特定のクラスまたは関連するクラスのグループのインスタンスが頻繁に作成および破棄され、実行時システムのデフォルトのメモリ管理が調整されていることを測定が示しているため、メモリ管理を微調整することです。一般的なパフォーマンスですが、この特定のケースでは非効率的です。これを改善するために、特定のクラスに対して new と delete をオーバーロードできます。

class my_class { 
  public: 
    // ... 
    void* operator new(std::size_t);
    void  operator delete(void*);
    void* operator new[](std::size_t);
    void  operator delete[](void*);
    // ...  
}; 

このようにオーバーロードされると、new と delete は静的メンバー関数のように動作します。のオブジェクトのmy_class場合、std::size_t引数は常に になりますsizeof(my_class)ただし、これらの演算子は、派生クラスの動的に割り当てられたオブジェクトに対しても呼び出されます。その場合、それよりも大きくなる可能性があります。

グローバルな新規および削除

グローバルな new と delete をオーバーロードするには、標準ライブラリの事前定義された演算子を独自のものに置き換えるだけです。ただし、これが必要になることはめったにありません。

于 2010-12-12T13:07:17.567 に答える
50

operator<<オブジェクトをファイルにストリーミングしstd::coutたり、ファイルにストリーミングしたりする関数をメンバー関数にできないのはなぜですか?

あなたが持っているとしましょう:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

そのため、次のものは使用できません。

Foo f = {10, 20.0};
std::cout << f;

operator<<のメンバー関数としてオーバーロードされているためFoo、演算子の LHS はFooオブジェクトでなければなりません。つまり、以下を使用する必要があります。

Foo f = {10, 20.0};
f << std::cout

これは非常に直感的ではありません。

非メンバ関数として定義すると、

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

以下を使用できます。

Foo f = {10, 20.0};
std::cout << f;

これは非常に直感的です。

于 2016-01-22T19:00:54.003 に答える