Rottura del cambiamento in C ++ 20 o regressione in clang-trunk / gcc-trunk quando si sovraccarica il confronto di uguaglianza con un valore di ritorno non booleano?


11

Il codice seguente viene compilato correttamente con clang-trunk in modalità c ++ 17 ma si interrompe in modalità c ++ 2a (imminente c ++ 20):

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

Si compila anche bene con gcc-trunk o clang-9.0.0: https://godbolt.org/z/8GGT78

L'errore con clang-trunk e -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)

Comprendo che C ++ 20 renderà possibile solo il sovraccarico operator==e il compilatore genererà automaticamente operator!=negando il risultato di operator==. Per quanto ho capito, questo funziona solo finché è il tipo restituito bool.

La fonte del problema è che in Eigen si dichiara un insieme di operatori ==, !=, <, ... tra Arrayoggetti o Arraye scalari, che di ritorno (espressione di) un array bool(che può quindi accedere elemento-saggio, o altrimenti utilizzato ). Per esempio,

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

Contrariamente al mio esempio sopra, questo non riesce nemmeno con gcc-trunk: https://godbolt.org/z/RWktKs . Non sono ancora riuscito a ridurlo a un esempio non Eigen, che fallisce sia in clang-trunk che in gcc-trunk (l'esempio in alto è abbastanza semplificato).

Rapporto sui problemi correlati: https://gitlab.com/libeigen/eigen/issues/1833

La mia vera domanda: si tratta in realtà di un cambiamento sostanziale in C ++ 20 (ed esiste la possibilità di sovraccaricare gli operatori di confronto per restituire Meta-oggetti) o è più probabile una regressione in clang / gcc?


Risposte:


5

Il problema Eigen sembra ridursi a quanto segue:

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

I due candidati per l'espressione sono

  1. il candidato riscritto da operator==(const Scalar&, const Derived&)
  2. Base<X>::operator!=(const Scalar&) const

Per [over.match.funcs] / 4 , poiché operator!=non è stato importato nell'ambito di Xuna dichiarazione using , il tipo del parametro oggetto implicito per # 2 è const Base<X>&. Di conseguenza, # 1 ha una sequenza di conversione implicita migliore per quell'argomento (corrispondenza esatta, anziché conversione derivata-base). Selezionando # 1, il programma viene mal formato.

Possibili correzioni:

  • Aggiungi using Base::operator!=;a Derived, o
  • Cambia il operator==per prendere un const Base&invece di un const Derived&.

C'è un motivo per cui il codice effettivo non può restituire un booldal loro operator==? Perché questa sembra essere l'unica ragione per cui il codice è mal formato in base alle nuove regole.
Nicol Bolas

4
Il codice attuale comporta una operator==(Array, Scalar)che fa il confronto elemento-saggio e restituire un Arraydi bool. Non puoi trasformarlo in a boolsenza rompere tutto il resto.
TC

2
Questo sembra un po 'un difetto nello standard. Le regole per la riscrittura operator==non dovevano influenzare il codice esistente, ma lo fanno in questo caso, perché la verifica di un boolvalore di ritorno non fa parte della selezione dei candidati per la riscrittura.
Nicol Bolas l'

2
@NicolBolas: il principio generale che si segue è che il controllo è se si può fare qualcosa ( es . Invocare l'operatore), non se si dovrebbe , per evitare che le modifiche all'implementazione influenzino silenziosamente l'interpretazione di altro codice. Si scopre che i confronti riscritti rompono molte cose, ma soprattutto cose già discutibili e facili da risolvere. Quindi, nel bene e nel male, queste regole sono state comunque adottate.
Davis Herring l'

Wow, grazie mille, immagino che la tua soluzione risolverà il nostro problema (al momento non ho tempo di installare trunk gcc / clang con uno sforzo ragionevole, quindi controllerò se questo rompe qualcosa fino alle ultime versioni stabili del compilatore ).
CHTZ

11

Sì, il codice infatti si interrompe in C ++ 20.

L'espressione Foo{} != Foo{}ha tre candidati in C ++ 20 (mentre ce n'era solo uno in 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

Questo deriva dalle nuove regole del candidato riscritto in [over.match.oper] /3.4 . Tutti questi candidati sono fattibili, poiché i nostri Fooargomenti non lo sono const. Per trovare il miglior candidato possibile, dobbiamo esaminare i nostri tiebreakers.

Le regole pertinenti per la migliore funzione praticabile sono, da [over.match.best] / 2 :

Date queste definizioni, una funzione vitale F1è definita come una funzione migliore di un'altra funzione vitale F2se per tutti gli argomenti i, non è una sequenza di conversione peggio , e poi ICSi(F1)ICSi(F2)

  • [... molti casi irrilevanti per questo esempio ...] o, se non quello, allora
  • F2 è un candidato riscritto ([over.match.oper]) e F1 no
  • F1 e F2 sono candidati riscritti e F2 è un candidato sintetizzato con ordine inverso di parametri e F1 non lo è

#2e #3sono candidati riscritti, e #3ha invertito l'ordine dei parametri, mentre #1non è riscritto. Ma per arrivare a quel tiebreaker, dobbiamo prima superare quella condizione iniziale: per tutti gli argomenti le sequenze di conversione non sono peggiori.

#1è meglio che #2perché tutte le sequenze di conversione sono uguali (banalmente, perché i parametri della funzione sono uguali) ed #2è un candidato riscritto mentre #1non lo è.

Ma ... entrambe le coppie #1/ #3e #2/ #3 rimangono bloccate in quella prima condizione. In entrambi i casi, il primo parametro ha una sequenza di conversione migliore per #1/ #2mentre il secondo parametro ha una sequenza di conversione migliore per #3(il parametro che constdeve subire una constqualifica aggiuntiva , quindi ha una sequenza di conversione peggiore). Questo constflip-flop ci impedisce di preferire nessuno dei due.

Di conseguenza, l'intera risoluzione del sovraccarico è ambigua.

Per quanto ho capito, questo funziona solo finché è il tipo restituito bool.

Non è corretto Consideriamo incondizionatamente candidati riscritti e invertiti. La regola che abbiamo è, da [over.match.oper] / 9 :

Se un operator==candidato riscritto viene selezionato per risoluzione di sovraccarico per un operatore @, il suo tipo di ritorno deve essere cv bool

Cioè, consideriamo ancora questi candidati. Ma se il miglior candidato possibile è operator==quello che ritorna, diciamo, Metail risultato è sostanzialmente lo stesso che se quel candidato fosse eliminato.

Abbiamo Non vogliamo essere in uno stato in cui la risoluzione di sovraccarico avrebbe dovuto prendere in considerazione il tipo di ritorno. E in ogni caso, il fatto che il codice qui ritorni Metaè irrilevante - il problema esisterebbe anche se restituito bool.


Per fortuna, la correzione qui è semplice:

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

Una volta effettuati entrambi gli operatori di confronto const, non vi è più ambiguità. Tutti i parametri sono gli stessi, quindi tutte le sequenze di conversione sono banalmente uguali. #1ora batterebbe #3non per riscritto e #2ora batterebbe #3per non essere invertito - il che rende #1il miglior candidato possibile. Stesso risultato che abbiamo avuto in C ++ 17, solo qualche altro passo per arrivarci.


" Non volevamo essere in uno stato in cui la risoluzione del sovraccarico avrebbe dovuto considerare il tipo restituito. " Giusto per essere chiari, mentre la risoluzione del sovraccarico stessa non considera il tipo restituito, le successive operazioni riscritte lo fanno . Il codice di uno non è corretto se la risoluzione di sovraccarico selezionasse una riscrittura ==e il tipo restituito della funzione selezionata non lo è bool. Ma questo abbattimento non avviene durante la risoluzione del sovraccarico stesso.
Nicol Bolas,

In realtà è mal formato solo se il tipo di ritorno è qualcosa che non supporta l'operatore! ...
Chris Dodd il

1
@ChrisDodd No, deve essere esattamente cv bool(e prima di questo cambiamento, il requisito era la conversione contestuale in bool- ancora no !)
Barry

Sfortunatamente, questo non risolve il mio vero problema, ma perché non sono riuscito a fornire un MRE che in realtà descrive il mio problema. Accetterò questo e quando sarò in grado di ridurre correttamente il mio problema, farò una nuova domanda ...
Chtz

2
Sembra che una riduzione adeguata per il problema originale sia gcc.godbolt.org/z/tFy4qz
TC

5

[over.match.best] / 2 elenca la priorità dei sovraccarichi validi in un set. La sezione 2.8 ci dice che F1è meglio che F2se (tra molte altre cose):

F2è un candidato riscritto ([over.match.oper]) e F1non lo è

L'esempio mostra un esplicito operator<essere chiamato anche se operator<=>c'è.

E [over.match.oper] /3.4.3 ci dice che la candidatura operator==in questa circostanza è un candidato riscritto.

Tuttavia , i tuoi operatori dimenticano una cosa cruciale: dovrebbero essere constfunzioni. E renderli non constcausa gli aspetti precedenti della risoluzione del sovraccarico di entrare in gioco. Nessuna delle due funzioni è una corrispondenza esatta, poiché non devono constesserci constconversioni per argomenti diversi. Ciò provoca l'ambiguità in questione.

Una volta che li fai const, compila il tronco di Clang .

Non posso parlare con il resto di Eigen, poiché non conosco il codice, è molto grande e quindi non può stare in un MCVE.


2
Arriviamo al tiebreaker che hai elencato solo se ci sono conversioni ugualmente buone per tutti gli argomenti. Ma non ci sono: a causa della mancanza const, i candidati non invertiti hanno una sequenza di conversione migliore per il secondo argomento e il candidato invertito ha una sequenza di conversione migliore per il primo argomento.
Richard Smith,

@RichardSmith: Sì, era il tipo di complessità di cui parlavo. Ma non volevo davvero passare attraverso e leggere / interiorizzare quelle regole;)
Nicol Bolas il

In effetti, ho dimenticato l' constesempio minimo. Sono abbastanza sicuro che Eigen usi constovunque (o definizioni di classi esterne, anche con constriferimenti), ma devo controllare. Provo a scomporre il meccanismo generale che Eigen usa per un esempio minimo, quando trovo il tempo.
CHTZ

-1

Abbiamo problemi simili con i nostri file di intestazione Goopax. Compilare quanto segue con clang-10 e -std = c ++ 2a produce un errore del compilatore.

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

Fornire questi operatori aggiuntivi sembra risolvere il problema:

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
Non era qualcosa che sarebbe stato utile fare prima? Altrimenti, come sarebbe a == 0stato compilato ?
Nicol Bolas

Questo non è davvero un problema simile. Come ha sottolineato Nicol, questo già non è stato compilato in C ++ 17. Continua a non compilare in C ++ 20, solo per un motivo diverso.
Barry

Ho dimenticato di menzionare: Forniamo anche operatori membri: gpu_bool gpu_type<T>::operator==(T a) const;e gpu_bool gpu_type<T>::operator!=(T a) const;Con C ++ - 17, funziona benissimo. Ma ora con clang-10 e C ++ - 20, questi non si trovano più, e invece il compilatore tenta di generare i propri operatori scambiando gli argomenti, e fallisce, perché il tipo restituito non lo è bool.
Ingo Josopait
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.