Breaking Änderung in C ++ 20 oder Regression in Clang-Trunk / Gcc-Trunk beim Überladen des Gleichheitsvergleichs mit nicht-booleschem Rückgabewert?


11

Der folgende Code lässt sich gut mit Clang-Trunk im C ++ 17-Modus kompilieren, bricht jedoch im C ++ 2a-Modus (bevorstehendes C ++ 20) ab:

// Meta struct describing the result of a comparison
struct Meta {};

struct Foo {
    Meta operator==(const Foo&) {return Meta{};}
    Meta operator!=(const Foo&) {return Meta{};}
};

int main()
{
    Meta res = (Foo{} != Foo{});
}

Es lässt sich auch gut mit gcc-trunk oder clang-9.0.0 kompilieren: https://godbolt.org/z/8GGT78

Der Fehler mit Clang-Trunk und -std=c++2a:

<source>:12:19: error: use of overloaded operator '!=' is ambiguous (with operand types 'Foo' and 'Foo')
    Meta res = (f != g);
                ~ ^  ~
<source>:6:10: note: candidate function
    Meta operator!=(const Foo&) {return Meta{};}
         ^
<source>:5:10: note: candidate function
    Meta operator==(const Foo&) {return Meta{};}
         ^
<source>:5:10: note: candidate function (with reversed parameter order)

Ich verstehe, dass C ++ 20 es möglich macht, nur zu überladen, operator==und der Compiler automatisch generiert, operator!=indem er das Ergebnis von negiert operator==. Soweit ich weiß, funktioniert dies nur, solange der Rückgabetyp ist bool.

Die Ursache des Problems ist , dass in Eigen wir eine Reihe von Operatoren erklären ==, !=, <, ... zwischen ArrayObjekten oder Arrayund Skalare, die Rückkehr (ein Ausdruck) ein Array von bool(die dann elementweise zugegriffen werden kann, oder anderweitig verwendet werden ). Z.B,

#include <Eigen/Core>
int main()
{
  Eigen::ArrayXd a(10);
  a.setRandom();
  return (a != 0.0).any();
}

Im Gegensatz zu meinem obigen Beispiel schlägt dies sogar mit gcc-trunk fehl: https://godbolt.org/z/RWktKs . Ich habe es noch nicht geschafft, dies auf ein Nicht-Eigen-Beispiel zu reduzieren, das sowohl im Clang-Trunk als auch im Gcc-Trunk fehlschlägt (das Beispiel oben ist ziemlich vereinfacht).

Zugehöriger Problembericht: https://gitlab.com/libeigen/eigen/issues/1833

Meine eigentliche Frage: Ist dies tatsächlich eine bahnbrechende Änderung in C ++ 20 (und besteht die Möglichkeit, die Vergleichsoperatoren zu überladen, um Meta-Objekte zurückzugeben), oder handelt es sich eher um eine Regression in clang / gcc?


Antworten:


5

Das Eigenproblem scheint sich auf Folgendes zu reduzieren:

using Scalar = double;

template<class Derived>
struct Base {
    friend inline int operator==(const Scalar&, const Derived&) { return 1; }
    int operator!=(const Scalar&) const;
};

struct X : Base<X> {};

int main() {
    X{} != 0.0;
}

Die beiden Kandidaten für den Ausdruck sind

  1. der umgeschriebene Kandidat von operator==(const Scalar&, const Derived&)
  2. Base<X>::operator!=(const Scalar&) const

Per [over.match.funcs] / 4 ist der Typ des impliziten Objektparameters für # 2 , da er operator!=nicht Xdurch eine using-Deklaration in den Gültigkeitsbereich von importiert wurde const Base<X>&. Infolgedessen hat # 1 eine bessere implizite Konvertierungssequenz für dieses Argument (exakte Übereinstimmung statt Konvertierung von abgeleitet zu Basis). Wenn Sie # 1 auswählen, wird das Programm schlecht geformt.

Mögliche Korrekturen:

  • Hinzufügen using Base::operator!=;zu Derivedoder
  • Ändern Sie das operator==, um a const Base&anstelle von a zu nehmen const Derived&.

Gibt es einen Grund, warum der tatsächliche Code kein boolvon ihrem zurückgeben konnte operator==? Denn dies scheint der einzige Grund zu sein, warum der Code nach den neuen Regeln schlecht geformt ist.
Nicol Bolas

4
Der eigentliche Code beinhaltet einen operator==(Array, Scalar)elementweisen Vergleich und gibt einen Arrayvon zurück bool. Sie können das nicht in ein verwandeln, boolohne alles andere zu zerbrechen.
TC

2
Dies scheint ein bisschen wie ein Defekt im Standard. Die Regeln für das Umschreiben operator==sollten sich nicht auf vorhandenen Code auswirken, tun dies jedoch in diesem Fall, da die Prüfung auf einen boolRückgabewert nicht Teil der Auswahl von Kandidaten für das Umschreiben ist.
Nicol Bolas

2
@NicolBolas: Das allgemeine Prinzip ist, dass geprüft wird, ob Sie etwas tun können ( z. B. den Operator aufrufen), und nicht, ob Sie sollten , um zu vermeiden, dass Implementierungsänderungen die Interpretation anderen Codes stillschweigend beeinflussen. Es stellt sich heraus, dass umgeschriebene Vergleiche viele Dinge kaputt machen, aber meistens Dinge, die bereits fragwürdig waren und leicht zu beheben sind. Also, zum Guten oder zum Schlechten, diese Regeln wurden sowieso übernommen.
Davis Herring

Wow, vielen Dank, ich denke, Ihre Lösung wird unser Problem lösen (ich habe momentan keine Zeit, gcc / clang trunk mit angemessenem Aufwand zu installieren, daher werde ich nur prüfen, ob dies zu den neuesten stabilen Compilerversionen führt ).
chtz

11

Ja, der Code bricht tatsächlich in C ++ 20 ab.

Der Ausdruck Foo{} != Foo{}hat drei Kandidaten in C ++ 20 (während es in C ++ 17 nur einen gab):

Meta operator!=(Foo& /*this*/, const Foo&); // #1
Meta operator==(Foo& /*this*/, const Foo&); // #2
Meta operator==(const Foo&, Foo& /*this*/); // #3 - which is #2 reversed

Dies ergibt sich aus den neuen umgeschriebenen Kandidatenregeln in [over.match.oper] /3.4 . Alle diese Kandidaten sind lebensfähig, da unsere FooArgumente dies nicht sind const. Um den besten Kandidaten zu finden, müssen wir unsere Tiebreaker durchgehen.

Die relevanten Regeln für die bestmögliche Funktion lauten ab [over.match.best] / 2 :

In Anbetracht dieser Definitionen, eine tragfähige Funktion F1definierte eine bessere Funktion als eine andere tragfähige Funktion sein , F2wenn für alle Argumente i, als keine schlechte Umwandlungsfolge ist , und dann ICSi(F1)ICSi(F2)

  • [... viele irrelevante Fälle für dieses Beispiel ...] oder, wenn nicht, dann
  • F2 ist ein umgeschriebener Kandidat ([over.match.oper]) und F1 nicht
  • F1 und F2 sind umgeschriebene Kandidaten, und F2 ist ein synthetisierter Kandidat mit umgekehrter Reihenfolge der Parameter und F1 nicht

#2und #3sind umgeschriebene Kandidaten und #3haben die Reihenfolge der Parameter umgekehrt, während sie #1nicht umgeschrieben werden. Aber um zu diesem Tiebreaker zu gelangen, müssen wir zuerst diese Anfangsbedingung durchstehen: Für alle Argumente sind die Konvertierungssequenzen nicht schlechter.

#1ist besser als #2weil alle Konvertierungssequenzen gleich sind (trivial, weil die Funktionsparameter gleich sind) und #2ein umgeschriebener Kandidat ist, während dies #1nicht der Fall ist.

Aber ... beide Paare #1/ #3und #2/ #3 bleiben bei dieser ersten Bedingung hängen. In beiden Fällen hat der erste Parameter eine bessere Konvertierungssequenz für #1/, #2während der zweite Parameter eine bessere Konvertierungssequenz für hat #3(der Parameter, constder einer zusätzlichen constQualifizierung unterzogen werden muss, hat also eine schlechtere Konvertierungssequenz). Dieses constFlip-Flop führt dazu, dass wir keines von beiden bevorzugen können.

Infolgedessen ist die gesamte Überlastungsauflösung nicht eindeutig.

Soweit ich weiß, funktioniert dies nur, solange der Rückgabetyp ist bool.

Das stimmt nicht Wir betrachten bedingungslos umgeschriebene und rückgängig gemachte Kandidaten. Die Regel, die wir haben, lautet aus [over.match.oper] / 9 :

Wenn ein umgeschriebener operator==Kandidat durch Überlastungsauflösung für einen Bediener ausgewählt wird @, muss sein Rückgabetyp cv sein bool

Das heißt, wir betrachten diese Kandidaten immer noch. Wenn jedoch der am besten geeignete Kandidat ein Kandidat ist operator==, der beispielsweise zurückkehrt, ist Metadas Ergebnis im Grunde das gleiche, als ob dieser Kandidat gelöscht worden wäre.

Wir wollten nicht in einem Zustand sein, in dem die Überlastungsauflösung den Rückgabetyp berücksichtigen müsste. Und auf jeden Fall ist die Tatsache, dass der Code hier zurückgegeben Metawird, unerheblich - das Problem würde auch bestehen, wenn er zurückgegeben wird bool.


Zum Glück ist die Lösung hier einfach:

struct Foo {
    Meta operator==(const Foo&) const;
    Meta operator!=(const Foo&) const;
    //                         ^^^^^^
};

Sobald Sie beide Vergleichsoperatoren erstellt haben const, gibt es keine Mehrdeutigkeit mehr. Alle Parameter sind gleich, daher sind alle Konvertierungssequenzen trivial gleich. #1würde jetzt #3durch nicht umgeschriebenes #2schlagen und würde jetzt #3durch nicht rückgängig gemacht schlagen - was #1den besten lebensfähigen Kandidaten macht. Das gleiche Ergebnis wie in C ++ 17, nur noch ein paar Schritte, um dorthin zu gelangen.


" Wir wollten nicht in einem Zustand sein, in dem die Überlastungsauflösung den Rückgabetyp berücksichtigen müsste. " Nur um klar zu sein, während die Überlastungsauflösung selbst den Rückgabetyp nicht berücksichtigt, tun dies die nachfolgenden umgeschriebenen Operationen . Der eigene Code ist schlecht geformt, wenn die Überlastungsauflösung eine Umschreibung auswählen würde ==und der Rückgabetyp der ausgewählten Funktion nicht bool. Dieses Keulen tritt jedoch nicht während der Überlastungsauflösung selbst auf.
Nicol Bolas

Es ist eigentlich nur schlecht geformt, wenn der Rückgabetyp etwas ist, das den Bediener nicht unterstützt! ...
Chris Dodd

1
@ChrisDodd Nein, es muss genau sein cv bool(und vor dieser Änderung war die Anforderung eine kontextbezogene Konvertierung zu bool- immer noch nicht !)
Barry

Leider löst dies mein eigentliches Problem nicht, aber das lag daran, dass ich kein MRE angegeben habe, das mein Problem tatsächlich beschreibt. Ich werde das akzeptieren und wenn ich mein Problem richtig reduzieren kann, werde ich eine neue Frage stellen ...
chtz

2
Es sieht so aus, als ob eine angemessene Reduzierung für die ursprüngliche Ausgabe gcc.godbolt.org/z/tFy4qz
TC

5

[over.match.best] / 2 listet auf, wie gültige Überladungen in einem Satz priorisiert werden. Abschnitt 2.8 sagt uns , dass F1besser als ist , F2wenn (unter vielen anderen Dingen):

F2ist ein umgeschriebener Kandidat ([over.match.oper]) und F1ist es nicht

Das Beispiel dort zeigt ein explizites operator<Aufrufen, obwohl operator<=>es vorhanden ist.

Und [over.match.oper] /3.4.3 sagt uns, dass die Kandidatur von operator==unter diesen Umständen ein umgeschriebener Kandidat ist.

Allerdings , Ihre Betreiber vergessen eine entscheidende Sache: sie sollten constFunktionen. Und wenn sie nicht verwendet werden const, kommen frühere Aspekte der Überlastungsauflösung ins Spiel. Keine der beiden Funktionen conststimmt genau überein, da constfür verschiedene Argumente Nicht- Konvertierungen durchgeführt werden müssen. Das verursacht die fragliche Mehrdeutigkeit.

Sobald Sie sie erstellt haben const, wird Clang Trunk kompiliert .

Ich kann nicht mit dem Rest von Eigen sprechen, da ich den Code nicht kenne, er sehr groß ist und daher nicht in eine MCVE passt.


2
Wir gelangen nur dann zu dem von Ihnen aufgelisteten Tiebreaker, wenn für alle Argumente gleich gute Konvertierungen vorliegen. Aber es gibt keine: Aufgrund des Fehlens consthaben die nicht umgekehrten Kandidaten eine bessere Konvertierungssequenz für das zweite Argument und der umgekehrte Kandidat eine bessere Konvertierungssequenz für das erste Argument.
Richard Smith

@RichardSmith: Ja, das war die Komplexität, über die ich gesprochen habe. Aber ich wollte diese Regeln nicht wirklich durchgehen und lesen / verinnerlichen müssen;)
Nicol Bolas

In der Tat habe ich das constim Minimalbeispiel vergessen . Ich bin mir ziemlich sicher, dass Eigen constüberall verwendet (oder außerhalb von Klassendefinitionen, auch mit constReferenzen), aber ich muss das überprüfen. Ich versuche, den Gesamtmechanismus, den Eigen verwendet, auf ein minimales Beispiel zu reduzieren, wenn ich die Zeit finde.
6.

-1

Wir haben ähnliche Probleme mit unseren Goopax-Header-Dateien. Das Kompilieren des Folgenden mit clang-10 und -std = c ++ 2a erzeugt einen Compilerfehler.

template<typename T> class gpu_type;

using gpu_bool     = gpu_type<bool>;
using gpu_int      = gpu_type<int>;

template<typename T>
class gpu_type
{
  friend inline gpu_bool operator==(T a, const gpu_type& b);
  friend inline gpu_bool operator!=(T a, const gpu_type& b);
};

int main()
{
  gpu_int a;
  gpu_bool b = (a == 0);
}

Die Bereitstellung dieser zusätzlichen Operatoren scheint das Problem zu lösen:

template<typename T>
class gpu_type
{
  ...
  friend inline gpu_bool operator==(const gpu_type& b, T a);
  friend inline gpu_bool operator!=(const gpu_type& b, T a);
};

1
War das nicht etwas, das vorher nützlich gewesen wäre? Ansonsten , wie würden a == 0zusammengestellt haben ?
Nicol Bolas

Dies ist kein ähnliches Problem. Wie Nicol betonte, wurde dies in C ++ 17 bereits nicht kompiliert. Es wird weiterhin nicht in C ++ 20 kompiliert, nur aus einem anderen Grund.
Barry

Ich habe vergessen zu erwähnen: Wir bieten auch Mitgliedsoperatoren an: gpu_bool gpu_type<T>::operator==(T a) const;und gpu_bool gpu_type<T>::operator!=(T a) const;mit C ++ - 17 funktioniert dies einwandfrei. Aber jetzt mit clang-10 und C ++ - 20 werden diese nicht mehr gefunden, und stattdessen versucht der Compiler, seine eigenen Operatoren durch Austauschen der Argumente zu generieren, und dies schlägt fehl, da der Rückgabetyp dies nicht ist bool.
Ingo Josopait
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.