C ++コンパイラがoperator ==とoperator!=を定義しないのはなぜですか?


302

私はコンパイラーにできる限り多くの仕事をさせてくれるという大ファンです。単純なクラスを作成する場合、コンパイラーは以下を「無料」で提供できます。

  • デフォルトの(空の)コンストラクター
  • コピーコンストラクタ
  • デストラクタ
  • 代入演算子(operator=

しかし、operator==やなどの比較演算子を与えることはできませんoperator!=。例えば:

class foo
{
public:
    std::string str_;
    int n_;
};

foo f1;        // Works
foo f2(f1);    // Works
foo f3;
f3 = f2;       // Works

if (f3 == f2)  // Fails
{ }

if (f3 != f2)  // Fails
{ }

これには正当な理由がありますか?メンバーごとの比較の実行が問題になるのはなぜですか?もちろん、クラスがメモリを割り当てる場合は注意が必要ですが、単純なクラスの場合は、コンパイラがこれを実行できますか?


4
もちろん、デストラクタも無料で提供されています。
Johann Gerell、

23
Alex Stepanovは彼の最近の講演の1つで、特定の条件下で==デフォルトの自動割り当て(=)があるのと同じように、デフォルトの自動を持たないのは間違いであると指摘しました。(ロジックがために両方を適用するためのポインタについての引数が矛盾している===、だけではなく、秒)。
alfC

2
@becko A9のシリーズの1つです:youtube.com/watch ?v=k-meLQaYP5Y 、どちらの話をしたか覚えていません。C ++ 17 open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0221r0.html
alfC

1
@becko、それはどちらもA9で「コンポーネントによる効率的なプログラミング」または「プログラミング会話」の最初の1つであり、YouTubeで利用できます。
alfC 2016年

1
@becko実際には、ビューのアレックスのポイントを指し、以下の答えがありstackoverflow.com/a/23329089/225186
alfC

回答:


71

コンパイラーは、ポインター比較が必要か、深い(内部)比較が必要かを知りません。

実装せずにプログラマーが自分で実装する方が安全です。その後、彼らは好きなすべての仮定をすることができます。


292
その問題は、それが非常に有害であるコピーctorを生成することを止めません。
MSalters 2008年

78
コピーコンストラクタ(とoperator=比較演算子と同じ文脈では、一般的な作業) -である、あなたが実行した後という期待がありa = ba == b真実です。コンパイラがoperator==と同じ集計値のセマンティクスを使用してデフォルトを提供することは間違いなく理にかなっていoperator=ます。私はpaercebalが実際にはここで正しいと思いますoperator=(そしてコピーctor)はCの互換性のためだけに提供されており、状況を悪化させたくありませんでした。
Pavel Minaev、2009年

46
-1。もちろん、深い比較が必要です。プログラマーがポインター比較を望んでいる場合、彼は(&f1 ==&f2)と書きます
Viktor Sehr

62
ヴィクトル、私はあなたの反応を考え直すことをお勧めします。クラスFooにBar *が含まれている場合、コンパイラーはFoo :: operator ==がBar *のアドレスまたはBarの内容を比較する必要があるかどうかをどのようにして知るのでしょうか。
Mark Ingram

46
@マーク:ポインターが含まれている場合、ポインター値の比較は妥当です。値が含まれている場合、値の比較は妥当です。例外的な状況では、プログラマーがオーバーライドできます。これは、言語がintとpointer-to-intsの比較を実装するのと同じです。
Eamon Nerbonne '26 / 09/26

317

コンパイラーがデフォルトのコピー・コンストラクターを提供できる場合、同様のデフォルトを提供できるはずであるという議論operator==()は、ある程度の意味があります。この演算子にコンパイラが生成したデフォルトを提供しないという決定の理由は、「C ++の設計と進化」(セクション11.4.1-コピーの制御)でStroustrupがデフォルトのコピーコンストラクターについて述べたことによって推測できると思います:

個人的には、コピー操作がデフォルトで定義されていることを残念に思い、多くのクラスのオブジェクトのコピーを禁止しています。ただし、C ++はデフォルトの割り当てとコピーコンストラクターをCから継承しており、頻繁に使用されます。

したがって、「C ++にデフォルトがないのはなぜoperator==()ですか」の代わりに、「C ++にデフォルトの割り当てとコピーコンストラクタがあるのはなぜですか?」という質問であるはずです。 (おそらくC ++のほとんどのいぼの原因ですが、おそらくC ++の人気の主な理由でもあります)。

私自身の目的のために、私のIDEでは、新しいクラスに使用するスニペットにプライベート割り当て演算子とコピーコンストラクターの宣言が含まれているため、新しいクラスを生成するときに、デフォルトの割り当てとコピー操作を取得できません-宣言を明示的に削除する必要がありますprivate:コンパイラーがそれらを生成できるようにしたい場合は、これらの操作のセクションから。


29
いい答えだ。:私はC ++ 11には、むしろ代入演算子とプライベートコピーコンストラクタを作るよりも、あなたがこのようにこれらを完全に削除することができていることを指摘してちょうどたい Foo(const Foo&) = delete; // no copy constructorFoo& Foo=(const Foo&) = delete; // no assignment operator
karadoc

9
「しかし、C ++はデフォルトの割り当てとコピーコンストラクターをCから継承しました」これは、すべてのC ++型をこのように作成する必要がある理由を意味するものではありません。彼らは、これを単純な古いPODに制限するべきでした、すでにCにあるタイプだけ、これ以上。
thesaint 2014

3
C ++がのためstructにこれらの動作を継承した理由は確かに理解できますが、class動作が異なる(そしてまともな)動作にならないことを願っています。プロセスでは、それはまたの間に、より意味のある差を与えているだろうstructclass、デフォルトのアクセスの横を。
jamesdlin

@jamesdlinルールが必要な場合、dtorが宣言されている場合は、暗黙の宣言とアクターの定義および割り当てを無効にすることが最も理にかなっています。
Deduplicator

1
プログラマーにコンパイラーを明示的に命令してを作成するようにさせても、まだ害はありませんoperator==。この時点で、それはいくつかのボイラープレートコードの単なる構文糖です。この方法でプログラマがクラスフィールド間のポインタを見落とすのではないかと心配している場合は、それ自体が等価演算子を持つプリミティブ型とオブジェクトでのみ機能するという条件を追加できます。ただし、これを完全に禁止する理由はありません。
NO_NAME

93

C ++ 20であっても、コンパイラーは暗黙的に生成operator==しません

struct foo
{
    std::string str;
    int n;
};

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ill-formed

ただし、C ++ 20以降は明示的にデフォルト設定することができます。==

struct foo
{
    std::string str;
    int n;

    // either member form
    bool operator==(foo const&) const = default;
    // ... or friend form
    friend bool operator==(foo const&, foo const&) = default;
};

デフォルト==はメンバーごとに行われます==(デフォルトのコピーコンストラクターがメンバーごとにコピーを作成するのと同じ方法で)。新しい規則はまたの間に予想される関係を与える==!=。たとえば、上記の宣言では、両方を記述できます。

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ok!
assert(foo{"Anton", 1} != foo{"Anton", 2}); // ok!

この特定の機能(デフォルト設定operator==と対称性の間==とは!=)から来ている一つの提案でより広範な言語機能の一部でしたoperator<=>


これに関する最近の更新があるかどうか知っていますか?c ++ 17で利用できるようになりますか?
dcmm88

3
@ dcmm88残念ながら、C ++ 17では使用できません。答えを更新しました。
アントンサビン

2
同じことを許可する修正された提案(短い形式を除く)はC ++ 20でも行われます:)
Rakete1111

= defaultデフォルトで作成されないものについては、基本的にを指定する必要がありますよね?それは私にとってはオキシモロンのように聞こえます(「明示的なデフォルト」)。
artin

@artin言語に新しい機能を追加しても、既存の実装が中断されないので、理にかなっています。新しいライブラリ標準を追加したり、コンパイラーが実行できる新しいことを追加したりすることは1つです。以前は存在しなかった場所に新しいメンバー関数を追加するのは、まったく別の話です。プロジェクトを間違いから守るには、さらに多くの努力が必要です。個人的には、明示的なデフォルトと暗黙的なデフォルトを切り替えるためにコンパイラフラグを使用することをお勧めします。古いC ++標準からプロジェクトをビルドし、コンパイラフラグで明示的なデフォルトを使用します。すでにコンパイラを更新しているため、適切に構成する必要があります。新しいプロジェクトではそれを暗黙的にします。
MaciejZałucki

44

私見、「良い」理由はありません。この設計上の決定に同意する人が非常に多いのは、彼らが値ベースのセマンティクスの力を習得する方法を学んでいないためです。実装では生のポインタを使用するため、多くのカスタムコピーコンストラクタ、比較演算子、デストラクタを記述する必要があります。

適切なスマートポインター(std :: shared_ptrなど)を使用する場合、デフォルトのコピーコンストラクターは通常は問題なく、仮想のデフォルト比較演算子の明らかな実装も問題ありません。


39

Cがしなかったため、C ++は==をしなかったと回答しました。Cがデフォルト=のみを提供し、==を最初から提供しないのは、ここが理由です。Cはシンプルにしたかった:C実装= memcpyによって。ただし、==は埋め込みのためにmemcmpで実装できません。パディングは初期化されていないため、memcmpは同じであっても異なると言います。同じ問題が空のクラスにも存在します。memcmpは、空のクラスのサイズがゼロでないため、それらが異なると言います。上記から、Cでの==の実装は、Cでの=の実装よりも複雑であることがわかります。これに関するいくつかのコード。もし私が間違っていればあなたの訂正はありがたいです。


6
C ++はmemcpyを使用しませんoperator=-これはPODタイプでのみ機能しますが、C ++はoperator=非PODタイプにもデフォルトを提供します。
フレキソ

2
ええ、C ++はより洗練された方法で実装されました。Cが実装されているようです=単純なmemcpyを使用します。
リオウイング

この回答の内容は、マイケルのものと一緒にする必要があります。彼は質問を修正し、これが答えます。
Sgene9

27

このビデオでは、STLの作成者であるAlex Stepanovが13:00頃にこの質問に対処しています。要約すると、C ++の進化を見守った彼は次のように主張しています。

  • ==と!=が暗黙的に宣言されていないのは残念です(そしてBjarneは彼に同意します)。正しい言語はそれらの準備が整っているはずです(彼はさらに、==のセマンティクスを壊す!=を定義できないようにする必要があることを示唆しています)
  • これがその理由である理由は、Cにルーツがある(C ++の問題の多くと同様)。そこで、代入演算子はビットごとの代入で暗黙的に定義されますが、==の場合は機能しません。この詳細については、Bjarne Stroustrupの記事をご覧ください。
  • フォローアップの質問で、なぜメンバーごとの比較ではなかったのかと彼は驚くべきことを言いました:Cは自家製の言語であり、Ritchieのためにこれらを実装する男は、これを実装するのは難しいと彼に言った!

その後、彼は(遠い)将来的に==および!=が暗黙的に生成されると言います。


2
この遠い未来は2017年にも18年にも19年にもならないようです。あなたは私のドリフトをキャッチします...
UmNyobe

18

C ++ 20は、デフォルトの比較演算子を簡単に実装する方法を提供します。

cppreference.comの例:

class Point {
    int x;
    int y;
public:
    auto operator<=>(const Point&) const = default;
    // ... non-comparison functions ...
};

// compiler implicitly declares operator== and all four relational operators work
Point pt1, pt2;
if (pt1 == pt2) { /*...*/ } // ok, calls implicit Point::operator==
std::set<Point> s; // ok
s.insert(pt1); // ok
if (pt1 <= pt2) { /*...*/ } // ok, makes only a single call to Point::operator<=>

4
それらが順序付け操作のPoint例として使用されていることに驚いています。これは、2つのポイントと座標を順序付けするための合理的なデフォルトの方法がないためです...xy
pipe

4
@pipe要素の順序を気にしない場合は、デフォルトの演算子を使用することは意味があります。たとえば、std::setすべてのポイントが一意であり、std::set使用operator<のみであることを確認するために使用できます。
VLL

戻り値の型についてautoこの場合、それがstd::strong_orderingからであることを常に想定でき#include <compare>ますか?
kevinarpe

1
@kevinarpe戻り値の型はstd::common_comparison_category_tであり、このクラスではこれがデフォルトの順序(std::strong_ordering)になります。
vll

15

defaultを定義することはできませんが、通常は自分で定義する必要がある==default !=を定義でき==ます。このためには、次のことを行う必要があります。

#include <utility>
using namespace std::rel_ops;
...

class FooClass
{
public:
  bool operator== (const FooClass& other) const {
  // ...
  }
};

詳細については、http://www.cplusplus.com/reference/std/utility/rel_ops/を参照してください。

さらに、を定義するとoperator< 、<=、>、> =の演算子は、を使用するときにそこから推定できますstd::rel_ops

ただしstd::rel_ops、比較演算子は予期しない型に対して推定される可能性があるため、使用時には注意が必要です。

基本的な演算子から関連する演算子を推測するより好ましい方法は、boost :: operatorsを使用することです。

ブーストで使用されるアプローチは、スコープ内のすべてのクラスではなく、必要なクラスのみの演算子の使用法を定義するため、より優れています。

「+ =」から「+」を生成したり、「-=」から-を生成したりすることもできます(完全なリストはこちら


演算子!=を書いた後、私はデフォルトになりませんでした==。または私はそうしましたが、それは欠けていましたconst。自分でも書く必要があり、すべて順調でした。
ジョン

必要な結果を得るために、const-nessで遊ぶことができます。コードがなければ、何が悪いのかを言うのは難しいです。
sergtk

2
理由があります rel_opsC ++ 20で廃止されが。それが機能しない、少なくともどこでも機能しない、そして確かに一貫していないためです。sort_decreasing()コンパイルするための信頼できる方法はありません。一方、Boost.Operatorsは機能し、常に機能しています。
Barry

10

C ++ 0xにデフォルト関数の提案があるのでdefault operator==; 、これらのことを明示的にするのに役立つことがわかりました。


3
「特別なメンバー関数」(デフォルトコンストラクター、コピーコンストラクター、代入演算子、およびデストラクタ)のみが明示的にデフォルト設定できると思いました。彼らはこれを他のいくつかのオペレーターに拡張しましたか?
マイケルバー

4
ムーブコンストラクタもデフォルトにすることができますが、これはには当てはまらないと思いますoperator==。それは残念です。
Pavel Minaev、2009年

5

概念的には、平等を定義することは容易ではありません。PODデータの場合でも、フィールドは同じでも、オブジェクトが異なる(アドレスが異なる)場合でも、必ずしも同じであるとは限りません。これは実際には演算子の使用法に依存します。残念ながら、あなたのコンパイラは精神的ではなく、それを推測することはできません。

これに加えて、デフォルトの機能は足で自分を撃つための優れた方法です。あなたが説明するデフォルトは、基本的にはPOD構造体との互換性を保つためにあります。ただし、開発者がそれらを忘れたり、デフォルトの実装のセマンティクスを使用したりすると、十分な混乱が生じます。


10
POD構造にはあいまいさはありません。これらは、他のPODタイプと同じように動作する必要があります。つまり、値の等価性(参照の等価性)です。一つのint別のコピーCTORで作成は、作成されたものと同じです。struct2つのintフィールドの1つに対して行うべき論理的なことは、まったく同じように動作することです。
Pavel Minaev

1
@mgiuca:値として動作するすべての型を辞書または類似のコレクションのキーとして使用できるようにする普遍的な同値関係には、かなりの有用性があります。ただし、このようなコレクションは、保証付きの再帰的等価関係がないと有効に機能しません。私見、最良の解決策は、すべての組み込み型が賢明に実装できる新しい演算子を定義し、いくつかは同等を参照等価として定義し、他はターゲットのターゲットにチェーンすることを除いて、既存のものと同様のいくつかの新しいポインター型を定義することです等価演算子。
スーパーキャット2015年

1
@supercatアナロジーで言えば+、浮動小数点数に関連付けられないという点で、演算子とほぼ同じ引数をとることができます。つまり、FPの丸めが行われる方法が原因で(x + y) + z!= x + (y + z)になります。(間違いなく、これは==通常の数値の場合よりもはるかに悪い問題です。)すべての数値型(intを含む)で機能し、ほぼ同じ+であるが連想(何とかして)。しかし、その場合、それほど多くの人々を助けることなく、言語に膨らみと混乱を追加することになります。
mgiuca 2015年

1
@mgiuca:エッジケースを除いて非常によく似たものを持つことは、たいてい非常に有用であり、そのようなことを避けるための誤った取り組みは、非常に不必要な複雑さをもたらします。クライアントコードでエッジケースを1つの方法で処理する必要がある場合や、別の方法で処理する必要がある場合は、処理のスタイルごとにメソッドを用意すると、クライアントで多くのエッジケース処理コードを削除できます。あなたの類推については、固定サイズの浮動小数点値の演算を定義して、すべての場合で推移的な結果を生成する方法はありません(1980年代の言語のセマンティクスはより優れていましたが...
supercat

1
...その点で今日よりも)、したがって、彼らが不可能を実行しないという事実は驚きではありません。ただし、コピーできるすべてのタイプの値に普遍的に適用できる同値関係を実装することには、基本的な障害はありません。
スーパーキャット2015年

1

これには正当な理由がありますか?メンバーごとの比較の実行が問題になるのはなぜですか?

機能的には問題ないかもしれませんが、パフォーマンスの点では、デフォルトのメンバーごとの比較は、デフォルトのメンバーごとの割り当て/コピーよりも最適ではない傾向があります。割り当ての順序とは異なり、最初の等しくないメンバーは残りをスキップできることを意味するため、比較の順序はパフォーマンスに影響を与えます。したがって、通常は等しいメンバーがいくつかある場合は、それらを最後に比較し、コンパイラーはどのメンバーが等しい可能性が高いかを認識していません。

この例を考えてみましょう。ここで、verboseDescriptionは、考えられる天気の説明の比較的小さなセットから選択された長い文字列です。

class LocalWeatherRecord {
    std::string verboseDescription;
    std::tm date;
    bool operator==(const LocalWeatherRecord& other){
        return date==other.date
            && verboseDescription==other.verboseDescription;
    // The above makes a lot more sense than
     // return verboseDescription==other.verboseDescription
     //     && date==other.date;
    // because some verboseDescriptions are liable to be same/similar
    }
}

(もちろん、コンパイラーは、副作用がないことを認識した場合、比較の順序を無視する資格がありますが、おそらくそれ自身のより良い情報を持たないソースコードからもクエリを取得します。)


しかし、パフォーマンスの問題が見つかった場合に、最適化されたユーザー定義の比較を書くことを妨げられる人はいません。私の経験では、それは非常に少数のケースになります。
ピーター-モニカの復活

1

時間が経過してもこの質問への回答が完全なままになるように:C ++ 20以降、コマンドで自動的に生成できます auto operator<=>(const foo&) const = default;

すべての演算子(==、!=、<、<=、>、> =)が生成されます。詳細については、https://en.cppreference.com/w/cpp/language/default_comparisonsを参照してください。

オペレーターの見た目<=>から、宇宙船オペレーターと呼ばれています。また、C ++でspaceship <=>演算子が必要な理由を参照してください

編集:C ++ 11でも、その完全なコード例std::tieについてはhttps://en.cppreference.com/w/cpp/utility/tuple/tieを参照してくださいbool operator<(…)。動作するように変更された興味深い部分==は次のとおりです。

#include <tuple>

struct S {
………
bool operator==(const S& rhs) const
    {
        // compares n to rhs.n,
        // then s to rhs.s,
        // then d to rhs.d
        return std::tie(n, s, d) == std::tie(rhs.n, rhs.s, rhs.d);
    }
};

std::tie すべての比較演算子で機能し、コンパイラによって完全に最適化されます。


-1

私は、PODタイプのクラスの場合、コンパイラーがそれを行うことができることに同意します。ただし、単純に考えるとコンパイラが間違っている可能性があります。そのため、プログラマーに任せるのが良いでしょう。

2つのフィールドが一意であるPODケースが1回ありました。そのため、比較が真と見なされることは決してありません。ただし、必要な比較はペイロードでのみ比較する必要がありました-コンパイラが理解できなかったもの、またはそれ自体で理解できるもの。

その上-彼らは書くのに時間がかかりませんか?!

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