Melanggar perubahan dalam C ++ 20 atau regresi dalam dentang-trunk / gcc-trunk saat membebani perbandingan kesetaraan dengan nilai pengembalian non-Boolean?


11

Kode berikut mengkompilasi baik-baik saja dengan dentang-trunk dalam mode c ++ 17 tetapi istirahat dalam mode c ++ 2a (mendatang 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{});
}

Itu juga mengkompilasi dengan gcc-trunk atau clang-9.0.0: https://godbolt.org/z/8GGT78

Kesalahan dengan dentang-batang dan -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)

Saya mengerti bahwa C + + 20 akan memungkinkan untuk hanya kelebihan operator==dan kompiler akan secara otomatis menghasilkan operator!=dengan meniadakan hasil operator==. Sejauh yang saya mengerti, ini hanya berfungsi selama tipe kembalinya bool.

Sumber masalahnya adalah bahwa di Eigen kita mendeklarasikan satu set operator ==, !=, <, ... antara Arrayobjek atau Arraydan skalar, yang kembali (ekspresi) Array bool(yang kemudian dapat diakses elemen-bijaksana, atau digunakan jika ). Misalnya,

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

Berbeda dengan contoh saya di atas ini bahkan gagal dengan gcc-trunk: https://godbolt.org/z/RWktKs . Saya belum berhasil menguranginya menjadi contoh non-Eigen, yang gagal di dentang-trunk dan gcc-trunk (contoh di atas cukup disederhanakan).

Laporan masalah terkait: https://gitlab.com/libeigen/eigen/issues/1833

Pertanyaan saya yang sebenarnya: Apakah ini benar-benar perubahan besar pada C ++ 20 (dan apakah ada kemungkinan untuk membebani operator pembanding untuk mengembalikan Meta-objek), atau apakah itu lebih cenderung berupa regresi dalam dentang / gcc?


Jawaban:


5

Masalah Eigen tampaknya mengurangi sebagai berikut:

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

Dua kandidat untuk berekspresi adalah

  1. kandidat yang ditulis ulang dari operator==(const Scalar&, const Derived&)
  2. Base<X>::operator!=(const Scalar&) const

Per [over.match.funcs] / 4 , seperti operator!=yang tidak diimpor ke dalam lingkup Xdengan menggunakan-deklarasi , jenis parameter objek implisit untuk # 2 adalah const Base<X>&. Akibatnya, # 1 memiliki urutan konversi implisit yang lebih baik untuk argumen itu (kecocokan persis, daripada konversi yang diturunkan ke basis). Memilih # 1 kemudian menjadikan program tersebut salah bentuk.

Kemungkinan perbaikan:

  • Tambahkan using Base::operator!=;ke Derived, atau
  • Ubah operator==untuk mengambil const Base&alih - alih a const Derived&.

Apakah ada alasan mengapa kode aktual tidak dapat mengembalikan booldari mereka operator==? Karena itu tampaknya menjadi satu-satunya alasan kode ini dibuat dengan buruk di bawah aturan baru.
Nicol Bolas

4
Kode aktual melibatkan operator==(Array, Scalar)yang melakukan perbandingan elemen-bijaksana dan mengembalikan Arraydari bool. Anda tidak dapat mengubahnya menjadi booltanpa merusak yang lainnya.
TC

2
Ini sepertinya sedikit cacat dalam standar. Aturan untuk menulis ulang operator==tidak seharusnya memengaruhi kode yang ada, namun mereka melakukannya dalam kasus ini, karena pemeriksaan untuk boolnilai pengembalian bukan bagian dari pemilihan kandidat untuk penulisan ulang.
Nicol Bolas

2
@NicolBolas: Prinsip umum yang diikuti adalah bahwa pemeriksaan adalah untuk apakah Anda dapat melakukan sesuatu ( misalnya , memanggil operator), bukan apakah Anda harus , untuk menghindari perubahan implementasi yang secara diam-diam memengaruhi interpretasi kode lain. Ternyata perbandingan yang ditulis ulang merusak banyak hal, tetapi sebagian besar hal yang sudah dipertanyakan dan mudah diperbaiki. Jadi, baik atau buruk, aturan ini tetap diadopsi.
Davis Herring

Wow, terima kasih banyak, saya kira solusi Anda akan menyelesaikan masalah kami (saya tidak punya waktu untuk menginstal gcc / clang trunk dengan upaya yang masuk akal saat ini, jadi saya hanya akan memeriksa apakah ini memecah sesuatu hingga versi kompiler stabil terbaru) ).
chtz

11

Ya, sebenarnya kode itu istirahat di C ++ 20.

Ekspresi Foo{} != Foo{}memiliki tiga kandidat dalam C ++ 20 (sedangkan hanya ada satu di 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

Ini berasal dari aturan kandidat yang ditulis ulang di [over.match.oper] /3.4 . Semua kandidat itu layak, karena Fooargumen kami tidak const. Untuk menemukan kandidat yang terbaik, kita harus melalui tiebreak kita.

Aturan yang relevan untuk fungsi terbaik adalah dari [over.match.best] / 2 :

Mengingat definisi ini, fungsi layak F1didefinisikan sebagai fungsi yang lebih baik dari fungsi lain yang layak F2jika untuk semua argumen i, bukan urutan konversi lebih buruk dari , dan kemudian ICSi(F1)ICSi(F2)

  • [... banyak kasus yang tidak relevan untuk contoh ini ...] atau, jika tidak, maka
  • F2 adalah kandidat yang ditulis ulang ([over.match.oper]) dan F1 tidak
  • F1 dan F2 adalah kandidat yang ditulis ulang, dan F2 adalah kandidat yang disintesis dengan urutan parameter terbalik dan F1 tidak

#2dan #3merupakan kandidat yang ditulis ulang, dan #3telah membalik urutan parameter, sementara #1tidak ditulis ulang. Tetapi untuk mencapai tiebreak itu, kita harus terlebih dahulu melewati kondisi awal itu: untuk semua argumen urutan konversi tidak lebih buruk.

#1lebih baik daripada #2karena semua urutan konversi adalah sama (sepele, karena parameter fungsi adalah sama) dan #2merupakan kandidat yang ditulis ulang sementara #1tidak.

Tapi ... keduanya berpasangan #1/ #3dan #2/ #3 terjebak pada kondisi pertama itu. Dalam kedua kasus, parameter pertama memiliki urutan konversi yang lebih baik untuk #1/ #2sedangkan parameter kedua memiliki urutan konversi yang lebih baik untuk #3(parameter yang constharus menjalani constkualifikasi tambahan , sehingga memiliki urutan konversi yang lebih buruk). Ini constflip-flop menyebabkan kita tidak dapat memilih satu baik.

Sebagai hasilnya, resolusi overload keseluruhan ambigu.

Sejauh yang saya mengerti, ini hanya berfungsi selama tipe kembalinya bool.

Itu tidak benar. Kami tanpa syarat mempertimbangkan kandidat yang ditulis ulang dan dibalik. Aturan yang kita miliki adalah, dari [over.match.oper] / 9 :

Jika operator==kandidat yang ditulis ulang dipilih dengan resolusi kelebihan untuk operator @, jenis kembalinya adalah cv bool

Artinya, kami masih mempertimbangkan kandidat ini. Tetapi jika kandidat terbaik yang layak adalah operator==yang mengembalikan, katakanlah, Meta- hasilnya pada dasarnya sama dengan jika kandidat itu dihapus.

Kami tidak ingin berada dalam keadaan di mana resolusi kelebihan harus mempertimbangkan jenis kembali. Dan dalam hal apapun, fakta bahwa kode di sini kembali Metatidak penting - masalahnya juga akan ada jika dikembalikan bool.


Untungnya, perbaikannya di sini mudah:

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

Setelah Anda membuat kedua operator pembanding const, tidak ada lagi ambiguitas. Semua parameter sama, sehingga semua urutan konversi sepele sama. #1sekarang akan dikalahkan #3oleh bukan oleh ditulis ulang dan #2sekarang akan dikalahkan #3dengan tidak terbalik - yang membuat #1kandidat terbaik. Hasil yang sama dengan yang kami miliki di C ++ 17, hanya beberapa langkah lagi untuk sampai ke sana.


" Kami tidak ingin berada dalam keadaan di mana resolusi kelebihan harus mempertimbangkan jenis kembali. " Hanya untuk menjadi jelas, sementara resolusi kelebihan itu sendiri tidak mempertimbangkan jenis kembali, operasi penulisan ulang selanjutnya dilakukan . Kode seseorang salah bentuk jika resolusi kelebihan akan memilih yang ditulis ulang ==dan jenis kembalinya fungsi yang dipilih tidak bool. Tetapi pemusnahan ini tidak terjadi selama resolusi kelebihan itu sendiri.
Nicol Bolas

Ini sebenarnya hanya buruk jika jenis kembali adalah sesuatu yang tidak mendukung operator! ...
Chris Dodd

1
@ ChrisDodd Tidak, harus tepat cv bool(dan sebelum perubahan ini, persyaratannya adalah konversi kontekstual ke bool- masih belum !)
Barry

Sayangnya, ini tidak menyelesaikan masalah saya yang sebenarnya, tetapi itu karena saya gagal memberikan MRE yang sebenarnya menjelaskan masalah saya. Saya akan menerima ini dan ketika saya bisa mengurangi masalah saya dengan benar, saya akan mengajukan pertanyaan baru ...
chtz

2
Sepertinya pengurangan yang tepat untuk masalah aslinya adalah gcc.godbolt.org/z/tFy4qz
TC

5

[over.match.best] / 2 mencantumkan prioritas overload yang valid dalam satu set. Bagian 2.8 memberi tahu kita bahwa F1itu lebih baik daripada F2jika (di antara banyak hal lain):

F2adalah kandidat yang ditulis ulang ([over.match.oper]) dan F1tidak

Contoh di sana menunjukkan panggilan eksplisit operator<meskipun operator<=>ada di sana.

Dan [over.match.oper] /3.4.3 memberi tahu kita bahwa pencalonan operator==dalam keadaan ini adalah kandidat yang ditulis ulang.

Namun , operator Anda lupa satu hal penting: mereka harus constberfungsi. Dan membuatnya tidak constmenyebabkan aspek-aspek sebelumnya dari resolusi kelebihan beban ikut bermain. Fungsi tidak adalah sama persis, sebagai non const-to- constkonversi perlu terjadi untuk argumen yang berbeda. Itu menyebabkan ambiguitas yang dimaksud.

Setelah Anda membuatnya const, dentang kompilasi batang .

Saya tidak dapat berbicara dengan Eigen yang lain, karena saya tidak tahu kodenya, ini sangat besar, dan karenanya tidak dapat ditampung dalam MCVE.


2
Kami hanya membuka tiebreak yang Anda cantumkan jika ada konversi yang sama baiknya untuk semua argumen. Tetapi tidak ada: karena yang hilang const, kandidat yang tidak terbalik memiliki urutan konversi yang lebih baik untuk argumen kedua dan kandidat yang terbalik memiliki urutan konversi yang lebih baik untuk argumen pertama.
Richard Smith

@ RichardSmith: Ya, itu adalah jenis kompleksitas yang saya bicarakan. Tetapi saya tidak mau harus benar-benar membaca dan membaca / menginternalisasi aturan-aturan itu;)
Nicol Bolas

Memang, saya lupa constdalam contoh minimal. Saya cukup yakin bahwa Eigen menggunakan di constmana-mana (atau di luar definisi kelas, juga dengan constreferensi), tetapi saya perlu memeriksa. Saya mencoba menjabarkan keseluruhan mekanisme yang digunakan Eigen ke contoh minimal, ketika saya menemukan waktu.
chtz

-1

Kami memiliki masalah serupa dengan file header Goopax kami. Kompilasi yang berikut dengan dentang-10 dan -std = c ++ 2a menghasilkan kesalahan kompiler.

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

Menyediakan operator tambahan ini sepertinya dapat menyelesaikan masalah:

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
Bukankah ini sesuatu yang berguna untuk dilakukan sebelumnya? Kalau tidak, bagaimana bisa a == 0dikompilasi ?
Nicol Bolas

Ini sebenarnya bukan masalah yang serupa. Seperti yang ditunjukkan Nicol, ini sudah tidak dikompilasi dalam C ++ 17. Itu terus tidak dikompilasi dalam C ++ 20, hanya karena alasan yang berbeda.
Barry

Saya lupa menyebutkan: Kami juga menyediakan operator anggota: gpu_bool gpu_type<T>::operator==(T a) const;dan gpu_bool gpu_type<T>::operator!=(T a) const;Dengan C ++ - 17, ini berfungsi dengan baik. Tapi sekarang dengan dentang-10 dan C ++ - 20, ini tidak ditemukan lagi, dan sebagai gantinya kompiler mencoba untuk menghasilkan operator sendiri dengan menukar argumen, dan gagal, karena tipe kembalinya tidak bool.
Ingo Josopait
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.