C++での演算子のオーバーロードについて学んでいます。==
と!=
は、単にユーザー定義型用にカスタマイズできる特別な関数に過ぎないことがわかります。私の関心事は、なぜ、なぜ2つの別々の定義が必要なのですか? a == b
がtrueの場合、a != b
は自動的にfalseになります。逆の場合も同様です。定義上、a != b
は!(a == b)
であるため、他の可能性はありません。そして、私はこれが真実ではなかった状況を想像することができませんでした。しかし、おそらく私の想像力は限られているのでしょうか、それとも私は何かを知らないのでしょうか。
一方を他方に対して定義できることは知っていますが、これは私が求めていることではありません。また、オブジェクトを値で比較するのかアイデンティティで比較するのかの区別についても質問しません。あるいは、2つのオブジェクトが同時に等しくても等しくなくてもよいかどうか(これは間違いなくオプションではありません!これらのことは相互に排他的です)。私が尋ねているのはこれです:
2つのオブジェクトが等しいという質問をしても意味がありませんが、それらについて質問する等しいではない意味がない場合がありますか? (ユーザーの観点から、または実装者の観点から)
そのような可能性がないのであれば、なぜ地球上でこれらの2つの演算子が2つの異なる関数として定義されているのでしょうか。
a != b
がbool
以外の何かを返すとき、あなたはではない言語がa == b
を!(a == b)
に自動的に書き換えることを望みます。そしてそれをする理由はいくつかあります。
式ビルダーオブジェクトがあるかもしれません。ここでa == b
は比較を実行しないことを意図していませんが、単にa == b
を表す式ノードを構築するだけです。
a == b
は直接比較を行わないことを意図していませんが、実際に比較を行うために暗黙的または明示的にbool
に変換できるある種のlazy<bool>
を返す遅延評価があります。評価前に式を完全に最適化できるように式ビルダーオブジェクトと組み合わせることも可能です。
カスタム変数optional<T>
テンプレートクラスがあるかもしれません。オプション変数t
およびu
が与えられた場合、t == u
を許可したいがoptional<bool>
を返すようにします。
私は考えていなかったことがおそらくもっとあります。そしてこれらの例ではa == b
とa != b
の操作はどちらも意味がありますが、それでもa != b
は!(a == b)
と同じものではないので、別々の定義が必要です。
そのような可能性がないのであれば、なぜ地球上でこれらの2つの演算子が2つの異なる関数として定義されているのでしょうか。
あなたはそれらをオーバーロードすることができ、そしてそれらをオーバーロードすることによってあなたは彼らに彼らの元のものとは全く異なる意味を与えることができます。
たとえば、元々ビット単位の左シフト演算子であった演算子<<
を取り上げます。現在はstd::cout << something
のように挿入演算子としてオーバーロードされています。元のものとはまったく異なる意味です。
そのため、オーバーロードしたときに演算子の意味が変わることに同意しても、ユーザーが演算子==
の否定ではない意味を演算子!=
に与えないようにする理由はありません。これは紛らわしいかもしれません。
私の関心事は、なぜ、なぜ2つの別々の定義が必要なのでしょうか。
両方を定義する必要はありません。
それらが相互に排他的である場合でも、==
と<
を同時に定義することで簡潔にすることができます std :: rel_ops
参考文献から:
#include <iostream>
#include <utility>
struct Foo {
int n;
};
bool operator==(const Foo& lhs, const Foo& rhs)
{
return lhs.n == rhs.n;
}
bool operator<(const Foo& lhs, const Foo& rhs)
{
return lhs.n < rhs.n;
}
int main()
{
Foo f1 = {1};
Foo f2 = {2};
using namespace std::rel_ops;
//all work as you would expect
std::cout << "not equal: : " << (f1 != f2) << '\n';
std::cout << "greater: : " << (f1 > f2) << '\n';
std::cout << "less equal: : " << (f1 <= f2) << '\n';
std::cout << "greater equal: : " << (f1 >= f2) << '\n';
}
2つのオブジェクトが等しいという質問をしても意味がありますが、等しくないという質問をしても意味がない場合がありますか。
これらの演算子を平等に関連付けることがよくあります。
それが基本的な型に対してどのように振る舞うのかということですが、これがカスタムデータ型に対して振る舞うべきであるという義務はありません。あなたがしたくない場合でも、あなたはブール値を返す必要はありません。
私は人々が奇妙な方法でオペレータをオーバーロードするのを見ました、それが彼らのドメイン特有のアプリケーションのために理にかなっていることを見つけるためだけに。たとえインターフェースがそれらが相互に排他的であることを示しているように見えても、作者は特定の内部ロジックを追加したいかもしれません。
(ユーザーの観点から、または実装者の観点から)
私はあなたが特定の例が欲しいのを知っています、
それで、これは私が実用的であると思った キャッチテストフレームワーク からのものです:
template<typename RhsT>
ResultBuilder& operator == ( RhsT const& rhs ) {
return captureExpression<Internal::IsEqualTo>( rhs );
}
template<typename RhsT>
ResultBuilder& operator != ( RhsT const& rhs ) {
return captureExpression<Internal::IsNotEqualTo>( rhs );
}
これらの演算子は異なることをしているので、あるメソッドを他のメソッドとして定義しても意味がありません。これが行われる理由は、フレームワークが行われた比較を出力できるようにするためです。そのためには、オーバーロードされた演算子が使用された内容を把握する必要があります。
(a == b)
と(a != b)
があるいくつかの非常によく確立された規約があります 両方とも偽 必ずしも反対ではありません。特にSQLでは、NULLと比較するとNULLになります。trueでもfalseでもありません。
これは直感的に理解できないので、できればこの新しい例を作成することはおそらくお勧めできませんが、既存の規約をモデル化しようとしている場合は、そのために演算子を「正しく」動作させるオプションがあるコンテキスト。
私はあなたの質問の第二部、すなわち:
そのような可能性がないのであれば、なぜ地球上でこれらの2つの演算子が2つの異なる関数として定義されているのでしょうか。
開発者が両方をオーバーロードできるようにするのが理にかなっている理由の1つはパフォーマンスです。 ==
と!=
の両方を実装することで最適化を許可するかもしれません。それでx != y
は!(x == y)
より安いかもしれません。いくつかのコンパイラはあなたのためにそれを最適化することができるかもしれません、しかし、特にあなたが多くの分岐を伴う複雑なオブジェクトを含むならば、おそらくそうではないでしょう。
開発者が法と数学の概念を非常に真剣に考えるHaskellにおいてさえ、ここで見ることができるように==
と/=
の両方をオーバーロードすることはまだ許されています( http://hackage.haskell.org/ package/base-4.9.0.0/docs/Prelude.html#v:-61--61 - ):
$ ghci
GHCi, version 7.10.2: http://www.haskell.org/ghc/ :? for help
λ> :i Eq
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
-- Defined in `GHC.Classes'
これはおそらくマイクロ最適化と考えられますが、場合によっては保証されるかもしれません。
2つのオブジェクトが等しいという質問をしても意味がありますが、等しくないという質問をしても意味がない場合がありますか。 (ユーザーの観点から、または実装者の観点から)
それは意見です。多分それは違います。しかし、言語デザイナーは、全知ではないので、(少なくとも彼らにとって)それが理にかなっているかもしれない状況を思い付くかもしれない人々を制限しないことにしました。
編集に応じて;
つまり、あるタイプで
==
演算子を使用でき、!=
を使用できない場合、またはその逆の場合、およびそうするのが理にかなっている場合です。
generalでは、いいえ、意味がありません。通常、平等演算子と関係演算子はセットで提供されます。平等がある場合、不平等も同様です。以下、<=
などでより大きいなど。同様のアプローチが算術演算子にも適用され、一般に自然な論理セットにもなります。
これは std::rel_ops
名前空間で証明されています。等号演算子と小なり演算子を実装する場合、その名前空間を使用すると、元の実装演算子に関して実装された他の名前空間を使用できます。
つまり、一方が他方をすぐに意味しない、または他の面で実装できない条件または状況はありますか?はい、おそらくほとんどありませんが、それらはあります。繰り返しますが、rel_ops
が独自の名前空間であることで証明されています。そのため、それらを個別に実装できるようにすることで、言語を活用して、コードのユーザーまたはクライアントにとって依然として自然で直感的な方法で、必要または必要なセマンティクスを取得できます。
すでに述べた遅延評価は、この優れた例です。別の良い例は、平等や不平等を意味しないセマンティクスをそれらに与えることです。これに似た例は、ビット挿入演算子<<
と>>
がストリームの挿入と抽出に使用されることです。一般的なサークルでは眉をひそめられるかもしれませんが、一部のドメイン固有の領域では意味があります。
==
および!=
演算子が実際に等価を意味しない場合、<<
および>>
ストリーム演算子がビットシフトを意味しないのと同様に。シンボルを他の概念を意味するかのように扱う場合、それらは相互に排他的である必要はありません。
同等性の観点から、ユースケースがオブジェクトを非比較対象として扱うことを保証するのであれば意味があります。そのため、すべての比較でfalseが返されます(演算子が非ブール値を返す場合は比較対象外の結果型)。これが正当化されるであろう特定の状況を考えることはできませんが、それが十分に合理的であることを私は見ることができました。
大きな力があれば、責任ある責任を持って、あるいは少なくとも本当に良いスタイルガイドが得られます。
==
と!=
はオーバーロードすることができます。それは祝福でもあり呪いでもあります。 !=
が!(a==b)
を意味するという保証はありません。
enum BoolPlus {
kFalse = 0,
kTrue = 1,
kFileNotFound = -1
}
BoolPlus operator==(File& other);
BoolPlus operator!=(File& other);
この演算子のオーバーロードを正当化することはできませんが、上の例ではoperator!=
をoperator==
の「反対」として定義することは不可能です。
結局、これらの演算子で調べているのは、式a == b
またはa != b
がブール値(true
またはfalse
)を返すことです。これらの式は、相互に排他的ではなく、比較後にブール値を返します。
[..]なぜ2つの別々の定義が必要なのですか?
考慮すべきことの1つは、これらの演算子の一方を他方の否定を使用するよりも効率的に実装する可能性があるということです。
(ここでの私の例はゴミですが、その意味はまだ残っています。例えばブルームフィルタについて考えてみましょう。セット内の何かがnotの場合は速いテストができます。もっと時間がかかる。)
[..]定義により、
a != b
は!(a == b)
です。
そしてそれを保持するのはプログラマーとしてのあなたの責任です。おそらくテストを書くのに良いことでしょう。
a != b
がfalseでa == b
がfalseというステートレスなビットのような、比較不可能なルールかもしれません。
if( !(a == b || a != b) ){
// Stateless
}
はい、1つは「同等」を意味し、もう1つは「非同等」を意味し、この用語は相互に排他的です。この演算子の他の意味は混乱しやすいので、絶対に避けてください。
演算子の振る舞いをカスタマイズすることによって、あなたは彼らにあなたが望むことをさせることができます。
あなたはものをカスタマイズしたいと思うかもしれません。たとえば、クラスをカスタマイズしたいと思うかもしれません。このクラスのオブジェクトは、特定のプロパティをチェックするだけで比較できます。これが事実であることを知っているので、あなたはオブジェクト全体の中のすべての単一のプロパティのすべての単一のビットをチェックする代わりに、最小のものだけをチェックするいくつかの特定のコードを書くことができます。
あなたが何かが同じであることを見つけることができるより速くではないにしても、何かが同じくらい速く異なることを理解できる場合を想像してみてください。確かに、何かが同じであるか違うかを理解したら、少し反転するだけで反対のことがわかります。ただし、そのビットを反転するのは余分な操作です。場合によっては、コードが大量に再実行されると、1回の操作を保存すると(何倍にもなる)、全体的な速度が向上することがあります。 (たとえば、メガピクセルの画面のピクセルごとに1つの操作を保存すると、100万回の操作を保存できたことになります。1秒間に60画面を掛けて、さらに多くの操作を保存できます。)
hvd's answer いくつかの追加の例を提供します。