演算子のオーバーロードの基本的なルールとイディオムは何ですか?


2144

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

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


63
C ++-FAQタグを続行する場合、これがエントリのフォーマット方法です。
John Dibling

演算子のオーバーロードに関するドイツのC ++コミュニティ向けの短いシリーズの記事を書きました:パート1:C ++での演算子のオーバーロードは、すべての演算子のセマンティクス、一般的な使用法、および専門をカバーしています。ここでの回答と重複していますが、追加情報があります。パート2および3では、Boost.Operatorsを使用するためのチュートリアルを作成します。それらを翻訳して回答として追加しますか?
Arne Mertz、2013

ああ、そして英語の翻訳も利用できます:基本一般的な練習
Arne Mertz 2017

回答:


1044

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

演算子のオーバーロードでの作業のほとんどは、ボイラープレートコードです。演算子は単なる構文上の糖なので、実際の作業は単純な関数によって行われる可能性があります(多くの場合、転送されます)。ただし、この定型コードを正しく取得することが重要です。失敗すると、オペレーターのコードがコンパイルされないか、ユーザーのコードがコンパイルされないか、ユーザーのコードが驚くほど動作します。

代入演算子

割り当てについては多くのことを言う必要があります。ただし、そのほとんどはGManの有名なコピーアンドスワップFAQですでに述べられているため、ここではそのほとんどを省略し、参照用に完全な代入演算子のみをリストします。

X& X::operator=(X rhs)
{
  swap(rhs);
  return *this;
}

ビットシフト演算子(ストリームI / Oに使用)

ビットシフト演算子<<とは>>、Cから継承するビット操作関数のハードウェアインターフェイスで引き続き使用されていますが、ほとんどのアプリケーションでオーバーロードされたストリーム入力および出力演算子として普及しています。ビット操作演算子としてのオーバーロードのガイダンスについては、以下の2項算術演算子のセクションを参照してください。オブジェクトをiostreamで使用するときに独自のカスタム形式と解析ロジックを実装する場合は、続行します。

ストリーム演算子は、最も一般的にオーバーロードされた演算子の1つであり、構文がメンバーまたは非メンバーであるかどうかの制限を指定しない2項中置演算子です。それらは左引数を変更する(ストリームの状態を変更する)ため、経験則に従って、左オペランドの型のメンバーとして実装する必要があります。ただし、それらの左側のオペランドは標準ライブラリからのストリームであり、標準ライブラリによって定義されたほとんどのストリーム出力および入力演算子は実際にストリームクラスのメンバーとして定義されていますが、独自のタイプの出力および入力操作を実装すると、標準ライブラリのストリームタイプを変更できません。そのため、独自の型に対してこれらの演算子を非メンバー関数として実装する必要があります。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 ++標準ライブラリ全体で、関数オブジェクトは常にコピーされます。したがって、独自の関数オブジェクトは安価にコピーできます。関数オブジェクトがコピーにコストのかかるデータを絶対に使用する必要がある場合は、そのデータを別の場所に格納し、関数オブジェクトにそれを参照させることをお勧めします。

比較演算子

バイナリのインフィックス比較演算子は、経験則に従って、非メンバー関数として実装する必要があります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項ブール演算子(||&&)をオーバーロードするための構文は、比較演算子の規則に従います。ただし、これらの2の合理的なユースケースを見つけることはほとんどありません。

1 すべての経験則と同様に、これを破る理由もある場合があります。その場合、メンバー関数ではとなる2項比較演算子の左側のオペランドも*thisである必要があることを忘れないでくださいconst。したがって、メンバー関数として実装された比較演算子には、次のシグネチャが必要です。

bool operator<(const X& rhs) const { /* do actual comparison with *this */ }

const最後のに注意してください。)

2 組み込みバージョンのショートカットセマンティクス||&&使用することに注意してください。ユーザーが定義したもの(メソッド呼び出しの構文上の砂糖のため)は、ショートカットセマンティクスを使用しないでください。ユーザーは、これらの演算子がショートカットセマンティクスを持つことを期待し、それらのコードはそれに依存する可能性があるため、絶対に定義しないことを強くお勧めします。

算術演算子

単項算術演算子

単項インクリメントおよびデクリメント演算子には、プレフィックスとポストフィックスの両方の種類があります。互いに区別するために、postfixバリアントは追加のダミーのint引数を取ります。インクリメントまたはデクリメントをオーバーロードする場合は、必ずプレフィックスバージョンとポストフィックスバージョンの両方を実装してください。以下は、インクリメントの標準的な実装です。デクリメントは同じルールに従います。

class X {
  X& operator++()
  {
    // do actual increment
    return *this;
  }
  X operator++(int)
  {
    X tmp(*this);
    operator++();
    return tmp;
  }
};

postfixバリアントは接頭辞に関して実装されることに注意してください。また、postfixは余分なコピーを行うことに注意してください。2

単項マイナスとプラスのオーバーロードはあまり一般的ではなく、おそらく回避するのが最善です。必要に応じて、メンバー関数としてオーバーロードする必要があります。

2 接尾辞バリアントは接頭辞バリアントよりも多くの作業を行うため、使用する効率が悪いことにも注意してください。これは、一般に接尾辞の増分よりも接頭辞の増分を優先する良い理由です。コンパイラーは通常、組み込み型の後置インクリメントの追加作業を最適化できますが、ユーザー定義型(リストイテレーターのように無邪気に見えるもの)に対しては同じことができない場合があります。に慣れると、が組み込み型ではない場合に代わりにi++行うことを覚えるのが非常に難しくなります(さらに、型を変更するときにコードを変更する必要があるため)、常に習慣をつけることをお勧めします接頭辞が明示的に必要でない限り、接頭辞の増分を使用します。++ii

二項算術演算子

あなたが提供する場合:バイナリ算術演算子については、第3の基本ルール演算子オーバーロード従うことを忘れないでください+、また提供+=、あなたが提供する場合は-省略しないでください、-=などアンドリュー・ケーニッヒは、化合物譲渡することを最初に観察していたと言われています演算子は、対応する非複合のベースとして使用できます。すなわち、オペレータがさ+の点で実現される+=-という点で実現されている-=

経験法則によれば+、そのコンパニオンは非メンバー+=である必要がありますが、左側の引数を変更するそれらの複合割り当ての対応物(など)はメンバーである必要があります。次に、+=およびのコード例を+示します。他の2項算術演算子も同じ方法で実装する必要があります。

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+を返す必要があります。3operator+は、その左オペランドをconst参照ではなくコピーで取得する ことにも注意してください。この理由は、operator=コピーごとに議論をする理由と同じです。

ビット操作演算子~ & | ^ << >>は、算術演算子と同じ方法で実装する必要があります。ただし、(オーバーロード<<および>>出力と入力を除いて)これらをオーバーロードするための合理的なユースケースはほとんどありません。

3 繰り返しますが、これから得られる教訓a += bは、一般的には、より効率的でa + bあり、可能であれば推奨することです。

配列の添え字

配列の添字演算子は、クラスメンバーとして実装する必要がある2項演算子です。キーによるデータ要素へのアクセスを許可するコンテナのようなタイプに使用されます。これらを提供する正規の形式は次のとおりです。

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(又はstruct又はunionタイプ)、他はoperator->()れるまで、再帰的に呼び出されoperator->()、非クラス型の値を返します。

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

以下のためのoperator->*()参照この質問を。これはめったに使用されないため、めったにオーバーロードされません。実際、イテレータでさえそれをオーバーロードしません。


進んで変換演算子


89
operator->()実際には非常に奇妙です。を返す必要はありませんvalue_type*-実際には、クラスタイプにがありoperator->()、その後に呼び出される場合、別のクラスタイプを返すことができます。このoperator->()sの再帰的な呼び出しは、value_type*戻り値の型が発生するまで続行されます。狂気!:)
j_random_hacker

2
それは正確には有効性についてではありません。これは、(非常に)少数のケースで従来の慣用的な方法でそれを行うことができないということです。結果を計算する間、両方のオペランドの定義を変更しないでおく必要がある場合です。そして、私が言ったように、2つの古典的な例があります:行列の乗算と多項式の乗算です。私たちは、定義することができ*という点で*=はなく最初の動作の一ので、それが厄介になる*=だろうが、計算の結果を新しいオブジェクトを作成します。次に、for-ijkループの後で、この一時オブジェクトをと交換し*thisます。すなわち。1.copy、2.operator *、3.swap
Luc

6
ポインタのような演算子のconst / non-constバージョンに同意しません。たとえば、 `const value_type&operator *()const;`-これは、逆参照時にonをT* const返すようなconst T&ものですが、そうではありません。または言い換えると、constポインタはconst指示先を意味しません。実際、模倣するのは簡単ではありませんT const *-これがconst_iterator標準ライブラリのすべてのものの理由です。結論:署名はreference_type operator*() const; pointer_type operator->() const
Arne Mertz 2013

6
1つのコメント:提案されている2項算術演算子の実装は、可能な限り効率的ではありません。Se Boostオペレーターヘッダーのシミュレーションの注意:boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry最初のパラメーターのローカルコピーを使用し、+ =を実行して、ローカルコピー。これにより、NRVOの最適化が可能になります。
Manu343726 2013

3
チャットで述べたように、の代わりにL <= Rとして表すこともできます。最適化が難しい式のインライン化の追加レイヤーを節約する可能性があります(また、Boost.Operatorsがそれを実装する方法でもあります)。!(R < L)!(L > R)
TemplateRex

494

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

C ++での演算子のオーバーロードに関しては、3つの基本的な規則に従う必要があります。そのようなすべてのルールと同様に、確かに例外があります。時々人々はそれらから逸脱し、結果は悪いコードではありませんでしたが、そのようなポジティブな逸脱はほとんどありません。少なくとも、私が見たこのような逸脱の100のうち99は正当化されていません。ただし、1000のうちの999である可能性もあります。そのため、次のルールに従うことをお勧めします。

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

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

  3. 関連する一連の操作のすべてを常に提供します。
    オペレーターは相互に関連し、他の操作に関連しています。型がをサポートしている場合a + b、ユーザーはa += bも呼び出すことができると予想されます。プレフィックスの増分をサポートしている場合は、同様に機能する++aことが期待a++されます。彼らがそうであるかどうかをチェックすることができればa < b、彼らはまた、もしかするとそれがチェックできることも期待できるでしょうa > b。彼らがあなたのタイプをコピー構築することができれば、彼らは割り当てがうまくいくことを期待しています。


会員と非会員の決定に進みます。


16
これらのいずれかに違反していることを私が知っている唯一のことはboost::spirit笑です。
Billy ONeal

66
@ビリー:一部の人によると+、文字列連結の乱用は違反ですが、今では十分に確立された慣行になっているため、自然なようです。私は90年代&にこの目的でバイナリを使用した自家製の文字列クラスを覚えています(確立された実践のためにBASICを参照)。しかし、ええ、それをstd libに入れると、基本的にこれがうまくいきます。悪用<<>>IO、BTWについても同様です。なぜ左シフトが明らかな出力操作になるのでしょうか?私たちが最初の「Hello、world!」を見たとき、私たちは皆それについて学んだからです。応用。そして、他に理由はありません。
sbi

5
@curiousguy:あなたがそれを説明しなければならない場合、それは明らかに明確で議論の余地はありません。同様に、オーバーロードについて議論または防御する必要がある場合。
sbi

5
@sbi:「ピアレビュー」は常に良い考えです。私にとって、ひどく選ばれた演算子は、ひどく選ばれた関数名と違いはありません(私はたくさん見ました)。演算子は単なる関数です。これ以上でもそれ以下でもない。ルールはまったく同じです。そして、アイデアが良いかどうかを理解するための最良の方法は、理解にかかる時間を理解することです。(したがって、ピアレビューは必須ですが、ピアは、教義と偏見のない人々の間で選択する必要があります。)
エミリオガラヴァグリア

5
@sbi私にとって、唯一明らかで明白な唯一の事実operator==は、それが同値関係であることです(IOW、非シグナルNaNは使用しないでください)。コンテナには多くの有用な同値関係があります。平等とはどういう意味ですか?「a等しいb」は、aそれbが同じ数学値を持つことを意味します。(非NaN)の数学的値の概念floatは明確ですが、コンテナーの数学的値は、多くの異なる(再帰的な)有用な定義を持つことができます。同等性の最も強力な定義は「それらは同じオブジェクトである」であり、それは役に立たない。
curiousguy

265

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

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

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

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

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

ただし、これらすべてをオーバーロードできるという事実は、そうする必要があるという意味ではありません。演算子のオーバーロードの基本的なルールをご覧ください。

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

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

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

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

3 、私はプレースホルダとしてそれを使用する理由であるC ++で有効な演算子ではありません。@

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


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


~単項接頭辞であり、二項接頭辞ではありません。
mrkj

1
.*オーバーロード不可能な演算子のリストから欠落しています。
celticminstrel 2015

1
@Mateen これは特別な演算子ではなく、すべての演算子に当てはまることを明確にするために、実際の演算子の代わりにプレースホルダーを使用したいと思いました。また、C ++プログラマーになりたい場合は、細かい部分にも注意を払う必要があります。:)
sbi 2015

1
@HR:このガイドを読めば、何が悪いのかわかるでしょう。一般に、質問からリンクされている最初の3つの回答を読むことをお勧めします。それはあなたの人生の30分以上であってはならず、あなたに基本的な理解を与えます。後で参照できる演算子固有の構文。特定の問題はoperator+()、メンバー関数としてオーバーロードしようとしたが、フリー関数のシグネチャを与えたことを示唆しています。こちらをご覧ください
sbi 2018

1
@sbi:最初の3つの投稿をすでに読んだので、作ってくれてありがとう。:)私は問題を解決しようとします。それ以外の場合は、別の質問で質問することをお勧めします。私たちの生活をとても簡単にしてくれてありがとう!:D
Hosein Rahnama 2018

251

会員と非会員の決定

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

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

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

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

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

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

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

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


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


9
効果的なC ++のHerb Sutterの項目(またはC ++コーディング標準ですか?)は、クラスのカプセル化を高めるために、メンバー関数よりも非メンバー非フレンド関数を優先する必要があると述べています。私見、カプセル化の理由は経験則よりも優先されますが、経験則の品質値は低下しません。
paercebal 2010

8
@paercebal:効果的なC ++はMeyers、C ++コーディング標準はSutterによるものです。どちらを指しているのですか?とにかく、operator+=()メンバーにならないというのは嫌いです。左側のオペランドを変更する必要があるため、定義上、内部を深く掘り下げる必要があります。メンバーにしないことで何が得られますか?
sbi

9
@sbi:C ++コーディング標準(サッター)の項目44は、メンバー以外の非フレンド関数を記述することを推奨します。もちろん、クラスのパブリックインターフェイスのみを使用してこの関数を実際に記述できる場合にのみ適用されます。できない場合(またはできるが、パフォーマンスが著しく低下する場合)は、メンバーまたはフレンドにする必要があります。
Matthieu M.10年

3
@sbi:おっと、効果的、例外的...名前を間違えても不思議ではありません。とにかく、オブジェクトのプライベート/保護されたデータにアクセスできる関数の数をできるだけ制限することで利益が得られます。このようにして、クラスのカプセル化を増やし、メンテナンス/テスト/進化を容易にします。
paercebal 2010

12
@sbi:一例。operator +=appendメソッドの両方を使用して、Stringクラスをコーディングするとします。appendあなたは私率n -1にインデックスからのパラメータの部分文字列を追加することができますので、この方法では、より完全である:append(string, start, end)持っている論理的なようだ+=と、コール追記をstart = 0してend = string.size。その時点では、appendはメンバーメソッドである可能性がありますが、メンバーであるoperator +=必要はありません。メンバー以外のメンバーにすることで、文字列の内部で使用するコードの量を減らすことができるので、良いことです。 ^ _ ^ ...
paercebal 2010

165

変換演算子(ユーザー定義変換とも呼ばれます)

C ++では、変換演算子(コンパイラーがユーザーの型と他の定義済みの型の間で変換できるようにする演算子)を作成できます。変換演算子には、暗黙的なものと明示的なものの2種類があります。

暗黙の変換演算子(C ++ 98 / C ++ 03およびC ++ 11)

暗黙的な変換演算子を使用すると、コンパイラーは、ユーザー定義型の値を他の型に暗黙的に変換できます(との間の変換intなどlong)。

以下は、暗黙の変換演算子を持つ単純なクラスです。

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

引数が1つのコンストラクタのような暗黙の変換演算子は、ユーザー定義の変換です。コンパイラーは、オーバーロードされた関数への呼び出しを照合するときに、ユーザー定義の変換を1つ許可します。

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

最初はこれは非常に役立つように見えますが、これの問題は、予期しないときに暗黙的な変換が実行されることさえあります。次のコードでvoid f(const char*)は、は左辺値my_string()はないため呼び出されます。したがって、最初のコードは一致しません。

void f(my_string&);
void f(const char*);

f(my_string());

初心者は簡単にこれを間違えやすく、経験の浅いC ++プログラマーでさえ、コンパイラーが疑わなかったオーバーロードを選択するため、時々驚かされます。これらの問題は、明示的な変換演算子によって軽減できます。

明示的な変換演算子(C ++ 11)

暗黙の変換演算子とは異なり、明示的な変換演算子は、予期しないときに実行されることはありません。以下は、明示的な変換演算子を持つ単純なクラスです。

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

に注意してくださいexplicit。暗黙の変換演算子から予期しないコードを実行しようとすると、コンパイラエラーが発生します。

prog.cpp:関数 'int main()'で:
prog.cpp:15:18:エラー: 'f(my_string)'の呼び出しに一致する関数がありません
prog.cpp:15:18:注:候補者は次のとおりです。
prog.cpp:11:10:注:void f(my_string&)
prog.cpp:11:10:注:「my_string」から「my_string&」への引数1の既知の変換はありません
prog.cpp:12:10:注:void f(const char *)
prog.cpp:12:10:注:「my_string」から「const char *」への引数1の既知の変換はありません

明示的なキャスト演算子を呼び出すにはstatic_cast、Cスタイルのキャスト、またはコンストラクタスタイルのキャスト(つまりT(value))を使用する必要があります。

ただし、これには1つの例外があります。コンパイラは暗黙的にに変換できboolます。さらに、コンパイラーは、変換後に別の暗黙的な変換を行うことはできませんbool(コンパイラーは、一度に2つの暗黙的な変換を行うことができますが、ユーザー定義の変換は最大で1つだけです)。

コンパイラーは「過去」をキャストしないためbool、明示的な変換演算子により、Safe Boolイディオムが不要になりました。たとえば、C ++ 11より前のスマートポインターは、セーフブールイディオムを使用して、整数型への変換を防止していました。C ++ 11では、コンパイラが明示的に型をブールに変換した後、コンパイラが暗黙的に整数型に変換することができないため、スマートポインタは明示的な演算子を使用します。

オーバーロードnewとにdelete進みます。


148

オーバーロードnewdelete

注:これは、オーバーロードおよびの構文のみを扱い、そのようなオーバーロードされた演算子の実装は扱いません。私が過負荷にの意味だと思う、自分のよくある質問を受けるに値する、私はそれ正義を行うことはできません演算子オーバーロードの話題の中、。newdeletenewdelete

基本

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

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

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

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 newoperator deleteそれを呼び出すつもりがない場合でも、常に一致するもオーバーロードする必要があります。その理由は、新しい式の評価中にコンストラクターがスローした場合、ランタイムシステムは、オブジェクトを作成するためにメモリを割り当てるために呼び出されoperator deleteoperator newものに一致するメモリを返します。一致するものを提供しない場合operator delete、デフォルトのものが呼び出されますが、これはほとんどの場合間違っています。および
をオーバーロードする場合は、配列バリアントのオーバーロードも検討する必要があります。newdelete

配置 new

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

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(); 

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

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

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

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

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

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

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

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


11
また、グローバルオペレーターのnewとdeleteを置き換えることは、通常、パフォーマンスのためであることに同意しません。逆に、それは通常、バグトレースのためです。
イットリル

1
また、オーバーロードされた新しい演算子を使用する場合は、一致する引数を持つ削除演算子も提供する必要があることに注意してください。あなたはそれがあまり興味のないグローバルな新しい/削除のセクションでそれを言います。
イットリル

13
@イットリルあなたは物事を混乱させています。意味は、オーバーロードされます。「演算子の過負荷」とは、意味が過負荷であることを意味します。それは文字通り関数がオーバーロードされることを意味しません、そして特にオペレーターnewはスタンダードのバージョンをオーバーロードしません。@sbiはその反対を主張しません。「加算演算子のオーバーロード」と言うのが一般的であるのと同じように、「新規のオーバーロード」と呼ぶのが一般的です。
Johannes Schaub-litb 2010

1
@sbi:参照してください(または、より良い、リンク)gotw.ca/publications/mill15.htm。それは時々nothrow新しいものを使う人々に対してのみ良い習慣です。
アレクサンドルC.

1
「一致する演算子の削除を指定しない場合、デフォルトの演算子が呼び出されます」->実際、引数を追加して一致する削除を作成しない場合、演算子の削除はまったく呼び出されず、メモリリークが発生します。(15.2.2、オブジェクトによって占有されているストレージは、適切な...オペレーターによる削除が見つかった場合にのみ割り当て解除されます)
dascandy

46

ファイルへの、またはファイルへの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;

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.