Le code suivant compile correctement avec clang-trunk en mode c ++ 17 mais se casse en mode c ++ 2a (c ++ 20 à venir):
// 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{});
}
Il compile également très bien avec gcc-trunk ou clang-9.0.0: https://godbolt.org/z/8GGT78
L'erreur avec clang-trunk et -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)
Je comprends que C ++ 20 ne permettra que de surcharger operator==
et que le compilateur générera automatiquement operator!=
en annulant le résultat de operator==
. Pour autant que je comprends, cela ne fonctionne que tant que le type de retour est bool
.
La source du problème est que nous déclarons Eigen un ensemble d'opérateurs ==
, !=
, <
, ... entre Array
objets ou Array
et Scalaires, qui reviennent (une expression de) un tableau de bool
(qui peut alors être accessible élément par élément, ou utilisé autrement ). Par exemple,
#include <Eigen/Core>
int main()
{
Eigen::ArrayXd a(10);
a.setRandom();
return (a != 0.0).any();
}
Contrairement à mon exemple ci-dessus, cela échoue même avec gcc-trunk: https://godbolt.org/z/RWktKs . Je n'ai pas encore réussi à réduire cela à un exemple non-Eigen, qui échoue à la fois dans clang-trunk et gcc-trunk (l'exemple en haut est assez simplifié).
Rapport de problème connexe: https://gitlab.com/libeigen/eigen/issues/1833
Ma vraie question: est-ce en fait un changement de rupture en C ++ 20 (et y a-t-il une possibilité de surcharger les opérateurs de comparaison pour renvoyer des méta-objets), ou est-ce plus probablement une régression dans clang / gcc?
Réponses:
Le problème propre semble se réduire à ce qui suit:
Les deux candidats à l'expression sont
operator==(const Scalar&, const Derived&)
Base<X>::operator!=(const Scalar&) const
Selon [over.match.funcs] / 4 , comme il
operator!=
n'a pas été importé dans la portée deX
par une déclaration using , le type du paramètre d'objet implicite pour # 2 estconst Base<X>&
. En conséquence, # 1 a une meilleure séquence de conversion implicite pour cet argument (correspondance exacte, plutôt que conversion dérivée en base). Sélectionner # 1 rend alors le programme mal formé.Corrections possibles:
using Base::operator!=;
àDerived
, ouoperator==
pour prendre unconst Base&
au lieu d'unconst Derived&
.la source
bool
de leuroperator==
? Parce que cela semble être la seule raison pour laquelle le code est mal formé selon les nouvelles règles.operator==(Array, Scalar)
qui effectue une comparaison par élément et renvoie unArray
ofbool
. Vous ne pouvez pas transformer cela en unbool
sans tout casser.operator==
n'étaient pas censées affecter le code existant, mais elles le font dans ce cas, car la vérification d'unebool
valeur de retour ne fait pas partie de la sélection des candidats à la réécriture.Oui, le code casse en fait en C ++ 20.
L'expression
Foo{} != Foo{}
a trois candidats en C ++ 20 (alors qu'il n'y en avait qu'un en C ++ 17):Cela vient des nouvelles règles du candidat réécrites dans [over.match.oper] /3.4 . Tous ces candidats sont viables, car nos
Foo
arguments ne le sont pasconst
. Afin de trouver le meilleur candidat viable, nous devons passer par nos bris d'égalité.Les règles pertinentes pour la meilleure fonction viable sont, à partir de [over.match.best] / 2 :
#2
et#3
sont des candidats réécrits, et#3
a inversé l'ordre des paramètres, alors qu'il#1
n'est pas réécrit. Mais pour arriver à ce bris d'égalité, nous devons d'abord passer par cette condition initiale: pour tous les arguments, les séquences de conversion ne sont pas pires.#1
est mieux que#2
parce que toutes les séquences de conversion sont les mêmes (trivialement, car les paramètres de fonction sont les mêmes) et#2
est un candidat réécrit alors que ce#1
n'est pas le cas.Mais ... les deux paires
#1
/#3
et#2
/#3
restent bloquées sur cette première condition. Dans les deux cas, le premier paramètre a une meilleure séquence de conversion pour#1
/#2
tandis que le deuxième paramètre a une meilleure séquence de conversion pour#3
(le paramètre quiconst
doit subir uneconst
qualification supplémentaire , donc il a une pire séquence de conversion). Cetteconst
bascule nous empêche de préférer l'un ou l'autre.Par conséquent, toute la résolution de surcharge est ambiguë.
Ce n'est pas correct. Nous considérons inconditionnellement les candidats réécrits et inversés. La règle que nous avons est, à partir de [over.match.oper] / 9 :
Autrement dit, nous considérons toujours ces candidats. Mais si le meilleur candidat viable est
operator==
celui qui revient, disons,Meta
- le résultat est fondamentalement le même que si ce candidat avait été supprimé.Nous ne pas voulons être dans un état où la résolution de surcharge devrait considérer le type de retour. Et en tout cas, le fait que le code retourne ici
Meta
est sans importance - le problème existerait également s'il revenaitbool
.Heureusement, la solution ici est simple:
Une fois que vous avez créé les deux opérateurs de comparaison
const
, il n'y a plus d'ambiguïté. Tous les paramètres sont les mêmes, donc toutes les séquences de conversion sont trivialement les mêmes.#1
serait désormais battu#3
non pas par réécriture et#2
serait désormais battu#3
en n'étant pas inversé - ce qui fait#1
le meilleur candidat viable. Même résultat que nous avions en C ++ 17, juste quelques étapes supplémentaires pour y arriver.la source
==
et le type de retour de la fonction sélectionnée ne l'est pasbool
. Mais cette élimination ne se produit pas pendant la résolution de surcharge elle-même.cv bool
(et avant ce changement, l'exigence était une conversion contextuelle enbool
- toujours pas!
)[over.match.best] / 2 répertorie la priorité des surcharges valides dans un ensemble. La section 2.8 nous dit que
F1
c'est mieux queF2
si (parmi beaucoup d' autres choses):L'exemple montre un
operator<
appel explicite même s'iloperator<=>
est là.Et [over.match.oper] /3.4.3 nous dit que la candidature de
operator==
dans cette circonstance est un candidat réécrit.Cependant , vos opérateurs oublient une chose cruciale: ils devraient être des
const
fonctions. Et ne pas les faireconst
entraîner des aspects antérieurs de la résolution de surcharge. Ni la fonction est une correspondance exacte, comme nonconst
-à-const
conversions doivent se produire pour différents arguments. Cela provoque l'ambiguïté en question.Une fois que vous les avez faites
const
, Clang trunk se compile .Je ne peux pas parler au reste d'Eigen, car je ne connais pas le code, il est très grand et ne peut donc pas tenir dans un MCVE.
la source
const
, les candidats non inversés ont une meilleure séquence de conversion pour le deuxième argument et le candidat inversé a une meilleure séquence de conversion pour le premier argument.const
dans l'exemple minimal. Je suis assez certain qu'Eigen utiliseconst
partout (ou en dehors des définitions de classe, également avec desconst
références), mais je dois vérifier. J'essaie de décomposer le mécanisme global qu'Eigen utilise en un exemple minimal, quand je trouve le temps.Nous avons des problèmes similaires avec nos fichiers d'en-tête Goopax. La compilation de ce qui suit avec clang-10 et -std = c ++ 2a produit une erreur de compilation.
Fournir ces opérateurs supplémentaires semble résoudre le problème:
la source
a == 0
compilé ?gpu_bool gpu_type<T>::operator==(T a) const;
etgpu_bool gpu_type<T>::operator!=(T a) const;
avec C ++ - 17, cela fonctionne très bien. Mais maintenant, avec clang-10 et C ++ - 20, ceux-ci ne sont plus trouvés, et à la place, le compilateur essaie de générer ses propres opérateurs en échangeant les arguments, et il échoue, car le type de retour ne l'est pasbool
.