Changement de rupture en C ++ 20 ou régression dans clang-trunk / gcc-trunk lors de la surcharge de comparaison d'égalité avec une valeur de retour non booléenne?

11

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 Arrayobjets ou Arrayet 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?

chtz
la source

Réponses:

5

Le problème propre semble se réduire à ce qui suit:

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;
}

Les deux candidats à l'expression sont

  1. le candidat réécrit de operator==(const Scalar&, const Derived&)
  2. Base<X>::operator!=(const Scalar&) const

Selon [over.match.funcs] / 4 , comme il operator!=n'a pas été importé dans la portée de Xpar une déclaration using , le type du paramètre d'objet implicite pour # 2 est const 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:

  • Ajouter using Base::operator!=;à Derived, ou
  • Modifiez le operator==pour prendre un const Base&au lieu d'un const Derived&.
TC
la source
Y a-t-il une raison pour laquelle le code réel n'a pas pu renvoyer un boolde leur operator==? Parce que cela semble être la seule raison pour laquelle le code est mal formé selon les nouvelles règles.
Nicol Bolas
4
Le code réel implique un operator==(Array, Scalar)qui effectue une comparaison par élément et renvoie un Arrayof bool. Vous ne pouvez pas transformer cela en un boolsans tout casser.
TC
2
Cela ressemble un peu à un défaut de la norme. Les règles de réécriture operator==n'étaient pas censées affecter le code existant, mais elles le font dans ce cas, car la vérification d'une boolvaleur de retour ne fait pas partie de la sélection des candidats à la réécriture.
Nicol Bolas
2
@NicolBolas: Le principe général suivi est que la vérification consiste à savoir si vous pouvez faire quelque chose ( par exemple , invoquer l'opérateur), pas si vous le devriez , pour éviter que les changements d'implémentation n'affectent silencieusement l'interprétation d'autres codes. Il s'avère que les comparaisons réécrites cassent beaucoup de choses, mais surtout des choses qui étaient déjà discutables et faciles à corriger. Donc, pour le meilleur ou pour le pire, ces règles ont quand même été adoptées.
Davis Herring
Wow, merci beaucoup, je suppose que votre solution résoudra notre problème (je n'ai pas le temps d'installer gcc / clang trunk avec un effort raisonnable pour le moment, donc je vais juste vérifier si cela casse quelque chose aux dernières versions du compilateur stable ).
chtz
11

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):

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

Cela vient des nouvelles règles du candidat réécrites dans [over.match.oper] /3.4 . Tous ces candidats sont viables, car nos Fooarguments ne le sont pas const. 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 :

Compte tenu de ces définitions, une fonction viable F1est définie comme étant une meilleure fonction qu'une autre fonction viable F2si, pour tous les arguments i, n'est pas une séquence de conversion pire que , puis ICSi(F1)ICSi(F2)

  • [... beaucoup de cas non pertinents pour cet exemple ...] ou, sinon, alors
  • F2 est un candidat réécrit ([over.match.oper]) et F1 n'est pas
  • F1 et F2 sont des candidats réécrits, et F2 est un candidat synthétisé avec un ordre de paramètres inversé et F1 n'est pas

#2et #3sont des candidats réécrits, et #3a inversé l'ordre des paramètres, alors qu'il #1n'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.

#1est mieux que #2parce que toutes les séquences de conversion sont les mêmes (trivialement, car les paramètres de fonction sont les mêmes) et #2est un candidat réécrit alors que ce #1n'est pas le cas.

Mais ... les deux paires #1/ #3et #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/ #2tandis que le deuxième paramètre a une meilleure séquence de conversion pour #3(le paramètre qui constdoit subir une constqualification supplémentaire , donc il a une pire séquence de conversion). Cette constbascule nous empêche de préférer l'un ou l'autre.

Par conséquent, toute la résolution de surcharge est ambiguë.

Pour autant que je comprends, cela ne fonctionne que tant que le type de retour est bool.

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 :

Si un operator==candidat réécrit est sélectionné par résolution de surcharge pour un opérateur @, son type de retour doit être cv bool

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 Metaest sans importance - le problème existerait également s'il revenait bool.


Heureusement, la solution ici est simple:

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

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. #1serait désormais battu #3non pas par réécriture et #2serait désormais battu #3en n'étant pas inversé - ce qui fait #1le meilleur candidat viable. Même résultat que nous avions en C ++ 17, juste quelques étapes supplémentaires pour y arriver.

Barry
la source
" Nous ne voulions pas être dans un état où la résolution de surcharge devrait prendre en compte le type de retour. " Juste pour être clair, alors que la résolution de surcharge elle-même ne prend pas en compte le type de retour, les opérations réécrites suivantes le font . Le code de l'un est mal formé si la résolution de surcharge sélectionnerait une réécriture ==et le type de retour de la fonction sélectionnée ne l'est pas bool. Mais cette élimination ne se produit pas pendant la résolution de surcharge elle-même.
Nicol Bolas
Ce n'est en fait mal formé que si le type de retour est quelque chose qui ne prend pas en charge l'opérateur! ...
Chris Dodd
1
@ChrisDodd Non, cela doit être exactement cv bool(et avant ce changement, l'exigence était une conversion contextuelle en bool- toujours pas !)
Barry
Malheureusement, cela ne résout pas mon problème réel, mais c'est parce que je n'ai pas fourni d'ERM qui décrit réellement mon problème. J'accepterai cela et quand je serai en mesure de réduire mon problème correctement, je poserai une nouvelle question ...
chtz
2
Il semble qu'une réduction appropriée pour le problème d'origine soit gcc.godbolt.org/z/tFy4qz
TC
5

[over.match.best] / 2 répertorie la priorité des surcharges valides dans un ensemble. La section 2.8 nous dit que F1c'est mieux que F2si (parmi beaucoup d' autres choses):

F2est un candidat réécrit ([over.match.oper]) et F1n'est pas

L'exemple montre un operator<appel explicite même s'il operator<=>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 constfonctions. Et ne pas les faire constentraîner des aspects antérieurs de la résolution de surcharge. Ni la fonction est une correspondance exacte, comme non const-à- constconversions 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.

Nicol Bolas
la source
2
Nous n'obtenons le bris d'égalité que vous avez indiqué que s'il existe des conversions également bonnes pour tous les arguments. Mais il n'y en a pas: en raison de l'absence 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.
Richard Smith
@ RichardSmith: Oui, c'est le genre de complexité dont je parlais. Mais je ne voulais pas avoir à lire et à internaliser ces règles;)
Nicol Bolas
En effet, j'ai oublié le constdans l'exemple minimal. Je suis assez certain qu'Eigen utilise constpartout (ou en dehors des définitions de classe, également avec des constré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.
chtz
-1

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.

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);
}

Fournir ces opérateurs supplémentaires semble résoudre le problème:

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);
};
Ingo Josopait
la source
1
N'était-ce pas quelque chose qu'il aurait été utile de faire au préalable? Sinon, comment aurait a == 0compilé ?
Nicol Bolas
Ce n'est pas vraiment un problème similaire. Comme l'a souligné Nicol, cela ne compilait pas déjà en C ++ 17. Il continue de ne pas compiler en C ++ 20, juste pour une raison différente.
Barry
J'ai oublié de mentionner: Nous fournissons également des opérateurs membres: gpu_bool gpu_type<T>::operator==(T a) const;et gpu_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 pas bool.
Ingo Josopait