c++ - 英語 - 演算子のオーバーロードの基本規則とイディオムは何ですか?




宇宙船演算子 英語 (5)

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

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

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

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

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

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

C ++では、演算子は特別な名前関数の形でオーバーロードされます 。 他の関数と同様に、オーバーロードされた演算子は、通常、その左のオペランドの型のメンバ関数として、または メンバ関数として実装できます 。 あなたが自由に選択することも、どちらかを使用することも、いくつかの基準に依存します。 2単項演算子@ 3は、オブジェクトxに適用され、 [email protected](x)または[email protected]()として呼び出されます。 オブジェクトxyに適用されるバイナリイン[email protected](y) [email protected](x,y)は、 [email protected](x,y)または[email protected](y)として[email protected](y)れます。 4

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

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

2 これはこのFAQの後半で説明します。

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

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

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

注:答えは特定の順序で与えられましが、多くのユーザーが投票に基づいて答えを並べ替えているので、回答の順序が最もわかりやすくなりました。

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


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

C ++での演算子のオーバーロードについては、 3つの基本的なルールがあります 。 このような規則のすべてと同様に、実際には例外があります。 時々人々はそれらから逸脱し、結果は悪いコードではありませんでしたが、そのような正の偏差はごくわずかです。 少なくとも、私が見た100の偏差のうち99は、正当化されていませんでした。 しかし、それはちょうど1000年のうち999年であったかもしれません。だから、あなたは次の規則に従うことをお勧めします。

  1. オペレータの意義がはっきりと明白ではないときはいつも、それは過負荷になってはいけません。 代わりに、適切に選択された名前の関数を提供してください。
    基本的に、オペレータを過負荷にするための最初の、そして最も重要なルールは、心の中で、 「しないでください 。 それはオペレータのオーバーロードについて知られるべきことがたくさんあるので、奇妙に思えるかもしれないので、多くの記事、本の章、および他のテキストがこのすべてを扱っています。 しかし、この明らかに明らかな証拠にもかかわらず、オペレータ過負荷が適切であるケースはごくわずかです。 その理由は、実際には、アプリケーションドメインのオペレータの使用がよく知られていて、明白でない限り、オペレータのアプリケーションの背後にあるセマンティクスを理解することは難しいからです。 普遍的な信念に反して、これはほとんど事実ではない。

  2. オペレータのよく知られているセマンティクスに常に従ってください。
    C ++では、オーバーロードされた演算子のセマンティクスに制限はありません。 あなたのコンパイラはバイナリ+演算子を実装するコードをうれしく受け取り、右のオペランドから引きます。 しかしながら、そのような演算子のユーザは、式a + bを決して疑うことはなく、 a + ba + bから減算a 。 もちろん、これは、アプリケーションドメイン内の演算子のセマンティクスが明白でないと仮定します。

  3. 関連する一連の操作のすべてを常に提供してください。
    演算子はお互いに 、そして他の演算に関連しています。 タイプa + bサポートしてa + b場合a += bも呼び出すことができるようになります。 接頭辞の増分++aサポートしている場合a++も同様に動作a++ことが期待されます。 彼らがa < bであるかどうかを確認することができれば、 a < b a > bかどうかを確認することも最も確実に期待される。 彼らがあなたのタイプをコピーすることができれば、彼らは割り当てがうまくいくと期待しています。

メンバーと非会員の決定に続きます。


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

オーバーロード演算子での作業の大半はボイラープレートコードです。 演算子は単なる構文的な砂糖なので、実際の作業は単純な関数で行うことができます。 しかし、このボイラープレートのコードを正しく入手することが重要です。 失敗した場合、オペレータのコードがコンパイルされないか、ユーザーのコードがコンパイルされないか、ユーザーのコードが驚くように動作します。

代入演算子

割り当てについて言われることはたくさんあります。 しかし、そのほとんどは既にGManの有名なCopy-And-Swap FAQで言われていますので、ここではそのほとんどを省略して、参照のために完全な代入演算子をリストアップします:

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

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

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

最も一般的にオーバーロードされる演算子の中のストリーム演算子は、バイナリ中置演算子であり、その構文では、メンバーがメンバーであるか非メンバーであるかに制限がないことが指定されています。 彼らは左の引数を変更するので(ストリームの状態を変更する)、左のオペランドの型のメンバとして実装する必要があります。 しかし、それらの左オペランドは標準ライブラリのストリームであり、標準ライブラリで定義されているほとんどのストリーム出力と入力演算子はストリームクラスのメンバーとして定義されていますが、独自の型の出力操作と入力操作を実装すると、標準ライブラリのストリームタイプを変更することはできません。 そのため、メンバー関数以外の関数として独自の型の演算子を実装する必要があります。 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>>実装する場合、ストリームの状態を手動で設定することは、読み込み自体が成功した場合にのみ必要ですが、結果は期待されるものではありません。

関数呼び出し演算子

ファンクション・オブジェクトを作成するために使用されるファンクション・コール演算子は、ファンクション・ファンクションとして定義する必要があります。 これ以外に、ゼロを含む任意の数の追加引数を取るためにオーバーロードすることができます。

構文の例を次に示します。

class foo {
public:
    // Overloaded call operator
    int operator()(const std::string& y) {
        // ...
    }
};

使用法:

foo f;
int a = f("hello");

C ++の標準ライブラリでは、関数オブジェクトは常にコピーされます。 したがって、独自の関数オブジェクトは安価にコピーする必要があります。 関数オブジェクトがコピーするのに高価なデータを絶対に使用する必要がある場合は、そのデータを他の場所に格納し、その関数オブジェクトを参照するようにしてください。

比較演算子

二項中置比較演算子は、経験則に従って、非メンバ関数として実装されるべきです。 単項接頭辞否定! (同じ規則に従って)メンバー関数として実装されるべきです。 (しかし、通常はオーバーロードするのは良い考えではありません)。

標準ライブラリのアルゴリズム(例: 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 すべての経験則と同様に、時にはこれを破る理由もあるかもしれません。 もしそうなら、バイナリ比較演算子の左辺オペランドはメンバー関数のために*thisconstである必要があることを忘れないでください。 したがって、メンバー関数として実装された比較演算子は、このシグネチャを持つ必要があります。

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

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

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

算術演算子

単項式演算子

単項インクリメントとデクリメント演算子は接頭辞と接尾辞フレーバーの両方に入ります。 一方を他方から伝えるために、後置変数は追加のダミーint引数を取る。 増分または減分をオーバーロードする場合は、常に接頭辞と接尾辞の両方のバージョンを実装するようにしてください。 増分の標準的な実装は次のとおりです。

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

postfixの変形は接頭辞の形で実装されていることに注意してください。 また、postfixは余分なコピーを作成します。 2

単項マイナスとプラスの積み上げはあまり一般的ではないので、おそらく最も避けてください。 必要に応じて、おそらくメンバー関数としてオーバーロードされるべきです。

2 また、postfixの亜種はもっと効くので、接頭辞の亜種よりも使用するのが効率的ではないことに注意してください。 これは、一般に接頭辞インクリメントを後置インクリメントよりも好む良い理由です。 コンパイラは、通常、組み込み型の後置インクリメントの追加作業を最適化することができますが、ユーザー定義型(これは無害なものとしてリストイテレータと見なされる可能性があります)では同じ処理を実行できない場合があります。 いったんあなたがi++をするのに慣れてしまったら、組み込み型ではなく(+型を変更するときにコードを変更する必要があります)+++をするのを忘れないようになります。接尾辞を明示的に必要としない限り、常に接頭辞のインクリメントを使用するという習慣を作ってください。

バイナリ算術演算子

バイナリ算術演算子の場合は、3番目の基本的なルール演算子のオーバーロードに従うことを忘れないでください。 +を指定する場合は、 +=-を指定しない場合は-=などを指定します。Andrew Koenigは、化合物代入演算子がそれらの非化合物対応物のベースとして使用できることを観察する。 つまり、operator ++=で実装され、 --=などで実装されます。

私たちの経験則によると、 +とその仲間は非会員でなければなりませんが、左の議論を変更している複合割り当てカウンターパート( +=等)はメンバーでなければなりません。 +=+コード例を示しますが、他の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+は新しい値を返す必要があります。 3また、 operator+はconst参照ではなくコピーによって左のオペランドを取ります。 これは、 operator=コピーごとに引数を取る理由と同じです。

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

3 ここでも、 a += bは一般に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 my_ptr {
        value_type& operator*();
  const value_type& operator*() const;
        value_type* operator->();
  const value_type* operator->() const;
};

これらも、ほとんど常にconstと非constバージョンの両方を必要とすることに注意してください。 ->演算子の場合、 value_typeclass (またはstructまたはunion struct )型の場合、 operator->()が非クラス型の値を返すまで、別のoperator->()が再帰的に呼び出されます。

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

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

コンバージョン演算子に進む


コンバージョン演算子(ユーザー定義コンバージョンとも呼ばれます)

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

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

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

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

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

最初はこれは非常に役に立つと思われますが、この問題は、暗黙の変換が期待されないときにさえ始まるということです。 次のコードでは、 my_string()lvalueではないため、最初のものが一致しないため、 void f(const char*)が呼び出されます。

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: In function ‘int main()’:
prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’
prog.cpp:15:18: note: candidates are:
prog.cpp:11:10: note: void f(my_string&)
prog.cpp:11:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘my_string&’
prog.cpp:12:10: note: void f(const char*)
prog.cpp:12:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘const char*’

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

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

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

Overloading new進みdeleteます。


ファイルへの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;

非常に直感的です。





c++-faq