Boolean olmayan dönüş değeri ile eşitlik karşılaştırması aşırı yüklenirken C ++ 20'de değişiklik mi yoksa clang-trunk / gcc-trunk'ta gerileme mi?


11

Aşağıdaki kod c ++ 17 modunda clang-trunk ile iyi derler, ancak c ++ 2a (yaklaşan c ++ 20) modunda kırılır:

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

Ayrıca gcc-trunk veya clang-9.0.0 ile iyi derler: https://godbolt.org/z/8GGT78

Clang-trunk ve ile ilgili hata -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)

C ++ 20'nin sadece aşırı yüklenmeyi mümkün kılacağını operator==ve derleyicinin operator!=sonucunu reddederek otomatik olarak üreteceğini anlıyorum operator==. Anladığım kadarıyla, bu sadece dönüş türü olduğu sürece çalışır bool.

Sorunun kaynağı Eigen içinde operatörlerin bir dizi ilan olmasıdır ==, !=, <arasında, ... Arraynesneleri veya Arrayve Skalarların, dönüş (bir sentezleme) bir dizi booldaha sonra öğeye erişilebilir veya başka bir şekilde kullanılabileceğini ( ). Örneğin,

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

Yukarıdaki örneğimin aksine, bu gcc-trunk ile bile başarısız oluyor: https://godbolt.org/z/RWktKs . Bunu hem clang-trunk hem de gcc-trunk'ta başarısız olan Eigen olmayan bir örneğe indirmeyi başaramadım (üstteki örnek oldukça basitleştirilmiş).

İlgili sorun raporu: https://gitlab.com/libeigen/eigen/issues/1833

Asıl sorum: Bu aslında C ++ 20'de bir kırılma değişikliği mi (ve Meta-nesneleri döndürmek için karşılaştırma işleçlerini aşırı yükleme olasılığı var mı) yoksa clang / gcc'de bir gerileme olasılığı daha mı yüksek?


Yanıtlar:


5

Öz sorunu şu şekilde azalıyor gibi görünüyor:

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

İfade için iki aday:

  1. yeniden yazılan aday operator==(const Scalar&, const Derived&)
  2. Base<X>::operator!=(const Scalar&) const

Ortalama [over.match.funcs] / 4 olarak operator!=kapsamına aktarılmamış Xbir yan kullanılarak beyanına , 2. için kapalı bir amacı parametresinin türü const Base<X>&. Sonuç olarak, # 1, bu bağımsız değişken için daha iyi bir örtülü dönüşüm sırasına sahiptir (türetilmiş-taban dönüşüm yerine tam eşleme). Daha sonra # 1 seçildiğinde program hatalı biçimlendirilir.

Olası düzeltmeler:

  • Ekle using Base::operator!=;için Derived, ya da
  • Değişim operator==bir almaya const Base&bir yerine const Derived&.

Gerçek kod neden bir boolonlardan dönememesinin bir nedeni var mı operator==? Çünkü bu, yeni kurallar altında kodun yanlış oluşturulmasının tek nedeni gibi görünüyor.
Nicol Bolas

4
Gerçek kod bir içerir operator==(Array, Scalar)öğeye göre karşılaştırma yapar ve bir dönüş olduğunu Arrayve bool. Bunu boolher şeyi kırmadan bir haline getiremezsiniz .
TC

2
Bu standartta bir kusur gibi görünüyor. Yeniden yazma kurallarının operator==mevcut kodu etkilememesi gerekiyordu, ancak bu durumda geçerliydi, çünkü bir booldönüş değeri kontrolü yeniden yazma adaylarının seçilmesinin bir parçası değil.
Nicol Bolas

2
@NicolBolas: Genel prensip takip ediliyor çek sen olmadığını içindir ki olabilir (bir şey yapmak örneğin sen olmasın, operatör çağırmak,) olmalıdır , sessizce diğer kodun yorumunu etkileyen değişiklikler uygulanmasını önlemek için. Yeniden yazılan karşılaştırmalar çok şey kırıyor, ancak çoğunlukla zaten tartışmalı ve düzeltilmesi kolay şeyler. Yani, daha iyisi ya da daha kötüsü, bu kurallar yine de kabul edildi.
Davis Herring

Vay canına, çok teşekkürler, çözümün sorunumuzu çözeceğini tahmin ediyorum (şu anda makul bir çaba ile gcc / clang gövdesini kurmak için zamanım yok, bu yüzden bunun en son derleyici sürümlerine kadar bir şey kırıp çözmediğini kontrol edeceğim ).
chtz

11

Evet, kod aslında C ++ 20'de kesiliyor.

İfadenin Foo{} != Foo{}C ++ 20'de üç adayı vardır (oysa C ++ 17'de sadece bir aday vardı):

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

Bu , [over.match.oper] /3.4'teki yeniden yazılan yeni aday kurallardan gelir . FooArgümanlarımız olmadığı için bu adayların hepsi uygulanabilir const. En uygun adayı bulmak için tiebreaklerimizden geçmeliyiz.

Geçerli en iyi işlev için ilgili kurallar [over.match.best] / 2'den :

Bu tanımlar göz önüne alındığında, tüm argümanlar için , daha kötü bir dönüşüm sırası değilse ve daha sonra F1başka bir geçerli fonksiyondan daha iyi bir fonksiyon olarak tanımlanır.F2iICSi(F1)ICSi(F2)

  • [... bu örnek için bir çok alakasız vaka ...] ya da değilse, o zaman
  • F2 yeniden yazılmış bir adaydır ([over.match.oper]) ve F1 değil
  • F1 ve F2 yeniden yazılan adaylardır ve F2 tersine çevrilmiş parametre sırasına sahip sentezlenmiş bir adaydır ve F1

#2ve #3yeniden yazılan adaylardır ve #3parametrelerin sırasını tersine çevirmişlerdir #1, ancak yeniden yazılmaz. Ancak bu tiebreak'e ulaşmak için önce bu ilk koşulu geçmemiz gerekir: tüm argümanlar için dönüşüm dizileri daha kötü değildir.

#1daha iyidir #2(işlev parametreleri aynı olduğu için, trivially) tüm dönüşüm dizileri aynı olduğu için ve #2sırasında yeniden yazılabilir bir adaydır #1değildir.

Ama ... hem çiftleri #1/ #3ve #2/ #3 o ilk koşuluyla takılıyorum. Her iki durumda da, birinci parametre için #1/ için daha iyi bir dönüşüm sekansına #2sahipken, ikinci parametre için daha iyi bir dönüşüm sekansına sahiptir #3( constekstra bir constyeterlilikten geçmesi gereken parametre , bu nedenle daha kötü bir dönüşüm sekansına sahiptir). Bu constflip-flop, ikisini de tercih etmemize neden oluyor.

Sonuç olarak, tüm aşırı yük çözünürlüğü belirsizdir.

Anladığım kadarıyla, bu sadece dönüş türü olduğu sürece çalışır bool.

Bu doğru değil. Koşulsuz olarak yeniden yazılan ve geri çevrilen adayları göz önünde bulunduruyoruz. Elimizdeki kural, [over.match.oper] / 9'dan :

Bir operator==operatör için aşırı yük çözünürlüğü ile yeniden yazılmış bir aday seçilirse @, geri dönüş tipi cv olacaktır. bool

Yani hala bu adayları düşünüyoruz. Ancak, en uygun aday operator==geri dönen bir yanıt ise , diyelim ki Meta- sonuç temel olarak bu adayın silinmiş olmasıyla aynıdır.

Biz did not aşırı yük çözünürlük dönüş türü dikkate almak zorunda kalacak nerede bir durumda olmak istiyorum. Ve her durumda, buradaki kodun geri Metadönmesi önemsizdir - sorun geri döndüğünde de var olacaktır bool.


Neyse ki, buradaki düzeltme kolaydır:

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

Her iki karşılaştırma operatörünü yaptıktan sonra const, artık belirsizlik yoktur. Tüm parametreler aynıdır, bu nedenle tüm dönüşüm dizileri önemsiz şekilde aynıdır. #1şimdi #3yeniden yazarak değil #2, şimdi #3tersine çevrilmeyerek yenilecekti - ki #1bu en uygun adayı yapıyor. Aynı sonuç C ++ 17'de, oraya ulaşmak için sadece birkaç adım daha vardı.


" Aşırı yük çözünürlüğünün dönüş türünü dikkate alması gereken bir durumda olmak istemedik. " Açık olmak gerekirse , aşırı yük çözünürlüğünün kendisi dönüş türünü dikkate almazken , sonraki yeniden yazılan işlemler bunu yapar . Aşırı yük çözünürlüğü yeniden yazılırsa ==ve seçilen işlevin dönüş türü seçilmezse, kod yanlış biçimlendirilir bool. Ancak bu kaldırma işlemi, aşırı yük çözünürlüğü sırasında gerçekleşmez.
Nicol Bolas

Aslında sadece dönüş tipi operatörü desteklemeyen bir
Chris Dodd

1
@ChrisDodd Hayır, tam olarak olmalı cv bool(ve bu değişiklikten önce, gereksinim bağlamsal dönüşüm bool- hala değil !)
Barry

Ne yazık ki, bu benim gerçek sorunumu çözmez, ama bunun nedeni aslında benim sorunumu açıklayan bir MRE sağlayamadı. Bunu kabul edeceğim ve sorunumu düzgün bir şekilde azaltabildiğimde yeni bir soru soracağım ...
chtz

2
Orijinal sorun için uygun bir azalma gibi görünüyor, gcc.godbolt.org/z/tFy4qz
TC

5

[over.match.best] / 2, bir kümedeki geçerli aşırı yüklenmelere nasıl öncelik verildiğini listeler. Bölüm 2.8 bize bunun (diğer birçok şeyden) F1daha iyi olduğunu söyler :F2

F2bir yeniden yazılan aday ([over.match.oper]) ve F1değildir

Oradaki örnek, orada operator<olmasına rağmen çağrılmış açık bir varlığı göstermektedir operator<=>.

Ve [over.match.oper] /3.4.3 bize bu durumda adaylığın operator==yeniden yazılmış bir aday olduğunu söyler .

Bununla birlikte , operatörleriniz önemli bir şeyi unutur: constfonksiyon olmalılar . Ve onları constaşırı yük çözünürlüğünün daha önceki yönlerinin ortaya çıkarmamasına neden olur. Her iki işlev de tam eşleşmedir, çünkü farklı argümanlar için dönüşüm constyapılmaması constgerekir. Bu, söz konusu belirsizliğe neden olmaktadır.

Onları yaptıktan sonra const, Clang gövdesi derlenir .

Eigen'in geri kalanıyla konuşamıyorum, kodu bilmediğim için çok büyük ve bu nedenle bir MCVE'ye sığamıyor.


2
Listelediğiniz tiebreaker'a yalnızca tüm argümanlar için eşit derecede iyi dönüşümler varsa ulaşırız. Ancak yoktur: eksik olması nedeniyle const, tersine çevrilmemiş adayların ikinci argüman için daha iyi bir dönüşüm sırası vardır ve tersine çevrilmiş adayın ilk argüman için daha iyi bir dönüşüm sırası vardır.
Richard Smith

@RichardSmith: Evet, bahsettiğim karmaşıklık buydu. Ama aslında bu kuralları okumak ve okumak / içselleştirmek zorunda kalmak istemedim;)
Nicol Bolas

Gerçekten, constminimal örnekte unuttum . Eigen consther yerde (veya constreferansları ile de dış sınıf tanımları) kullanır eminim , ama kontrol etmek gerekir. Zamanı bulduğumda Eigen'in kullandığı genel mekanizmayı minimal bir örneğe ayırmaya çalışıyorum.
chtz

-1

Goopax başlık dosyalarımızla benzer sorunlarımız var. Aşağıdakileri clang-10 ve -std = c ++ 2a ile derlemek bir derleyici hatası oluşturur.

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

Bu ek operatörleri sağlamak sorunu çözüyor gibi görünüyor:

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
Bu önceden yapılması faydalı olacak bir şey değil miydi? Aksi halde nasıl a == 0derlenirdi ?
Nicol Bolas

Bu gerçekten benzer bir konu değil. Nicol'in belirttiği gibi, bu zaten C ++ 17'de derlenmedi. Sadece farklı bir nedenden ötürü C ++ 20'de derlenmemeye devam ediyor.
Barry

Söylemeyi unuttum: Biz de üye operatörleri sağlar: gpu_bool gpu_type<T>::operator==(T a) const;ve gpu_bool gpu_type<T>::operator!=(T a) const;birlikte C ++ - 17, bu cezayı çalışır. Ancak şimdi clang-10 ve C ++ - 20 ile bunlar artık bulunamıyor ve bunun yerine derleyici argümanları değiştirerek kendi operatörlerini oluşturmaya çalışıyor ve başarısız oluyor, çünkü dönüş türü değil bool.
Ingo Josopait
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.