การเปลี่ยนแปลงที่เปลี่ยนแปลงใน C ++ 20 หรือการถดถอยใน clang-trunk / gcc-trunk เมื่อทำการโอเวอร์โหลดการเปรียบเทียบความเท่าเทียมกันกับค่าตอบแทนที่ไม่ใช่บูลีน?


11

รหัสต่อไปนี้รวบรวมดีกับ clang-trunk ในโหมด c ++ 17 แต่แบ่งในโหมด c ++ 2a (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{});
}

นอกจากนี้ยังรวบรวมการปรับด้วย gcc-trunk หรือ clang-9.0.0: https://godbolt.org/z/8GGT78

ข้อผิดพลาดกับ clang-trunk และ-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 จะทำให้มันเป็นไปได้เพียงเกินoperator==และเรียบเรียงโดยอัตโนมัติจะสร้างโดยกวนผลมาจากการoperator!= operator==เท่าที่ฉันเข้าใจนี่ใช้ได้ผลตราบใดที่ประเภทการคืนเป็นboolเท่านั้น

แหล่งที่มาของปัญหาคือว่าใน Eigen เราประกาศชุดประกอบ==, !=, <... ระหว่างArrayวัตถุหรือArrayและสเกลาซึ่งผลตอบแทน (การแสดงออกของ) อาร์เรย์ของbool(ซึ่งจากนั้นจะสามารถเข้าถึงองค์ประกอบฉลาดหรือนำไปใช้อย่างอื่น ) เช่น,

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

ในทางตรงกันข้ามกับตัวอย่างของฉันข้างต้นนี้แม้จะล้มเหลวด้วย GCC-ลำต้น: https://godbolt.org/z/RWktKs ฉันยังไม่ได้จัดการเพื่อลดสิ่งนี้ให้เป็นตัวอย่างที่ไม่ใช่ของ Eigen ซึ่งล้มเหลวทั้งใน clang-trunk และ gcc-trunk (ตัวอย่างที่ด้านบนค่อนข้างง่าย)

รายงานปัญหาที่เกี่ยวข้อง: https://gitlab.com/libeigen/eigen/issues/1833

คำถามที่แท้จริงของฉัน: นี่คือการเปลี่ยนแปลงจริงใน C ++ 20 (และมีความเป็นไปได้ที่จะโอเวอร์โหลดตัวดำเนินการเปรียบเทียบเพื่อส่งคืน Meta-object) หรือมีแนวโน้มว่าจะถดถอยใน clang / gcc หรือไม่


ที่เกี่ยวข้อง: stackoverflow.com/questions/58319928/…
chtz

คำตอบ:


5

ปัญหา Eigen ปรากฏขึ้นเพื่อลดสิ่งต่อไปนี้:

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

ผู้สมัครสองคนสำหรับการแสดงออกคือ

  1. ผู้สมัครเขียนใหม่จาก operator==(const Scalar&, const Derived&)
  2. Base<X>::operator!=(const Scalar&) const

ต่อ[over.match.funcs] / 4เป็นoperator!=ไม่ได้นำเข้ามาในขอบเขตของXโดยใช้ประกาศประเภทของพารามิเตอร์วัตถุโดยปริยายสำหรับ # const Base<X>&2 ด้วยเหตุนี้ # 1 จึงมีลำดับการแปลงโดยนัยที่ดีกว่าสำหรับอาร์กิวเมนต์นั้น (การจับคู่แบบตรงทั้งหมดแทนที่จะเป็นการแปลงที่ได้รับมาเป็นฐาน) เลือก # 1 จากนั้นแสดงผลโปรแกรมที่ไม่ถูกต้อง

การแก้ไขที่เป็นไปได้:

  • เพิ่มusing Base::operator!=;ไปยังDerivedหรือ
  • เปลี่ยนoperator==ไปใช้แทนconst Base&const Derived&

มีเหตุผลที่รหัสจริงไม่สามารถส่งคืนboolจากพวกเขาoperator==? เนื่องจากเป็นเหตุผลเดียวที่ทำให้รหัสไม่ถูกต้องภายใต้กฎใหม่
Nicol Bolas

4
รหัสจริงเกี่ยวข้องกับการoperator==(Array, Scalar)ที่จะเปรียบเทียบองค์ประกอบที่ชาญฉลาดและกลับของArray boolคุณไม่สามารถเปลี่ยนสิ่งนั้นให้กลายเป็น a ได้boolโดยไม่ทำลายทุกอย่างอื่น
TC

2
ดูเหมือนว่าจะเป็นข้อบกพร่องเล็กน้อยในมาตรฐาน กฎสำหรับการเขียนใหม่operator==ไม่ควรส่งผลกระทบต่อรหัสที่มีอยู่ แต่จะทำในกรณีนี้เนื่องจากการตรวจสอบboolค่าส่งคืนไม่ได้เป็นส่วนหนึ่งของการเลือกผู้สมัครสำหรับการเขียนใหม่
Nicol Bolas

2
@NicolBolas: หลักการทั่วไปที่ปฏิบัติตามคือการตรวจสอบว่าคุณสามารถทำบางสิ่ง ( เช่นเรียกใช้โอเปอเรเตอร์) ไม่ว่าคุณควรจะหลีกเลี่ยงการเปลี่ยนแปลงของการใช้งานที่มีผลต่อการแปลรหัสอื่น ๆ ปรากฎว่าการเปรียบเทียบที่เขียนขึ้นใหม่นั้นทำลายหลายสิ่งหลายอย่าง แต่ส่วนใหญ่เป็นสิ่งที่น่าสงสัยอยู่แล้วและง่ายต่อการแก้ไข ดังนั้นเพื่อให้ดีขึ้นหรือแย่ลงกฎเหล่านี้จึงถูกนำมาใช้
Davis Herring

ว้าวขอบคุณมากฉันคิดว่าทางออกของคุณจะแก้ไขปัญหาของเรา (ฉันไม่มีเวลาติดตั้ง gcc / clang trunk ด้วยความพยายามที่สมเหตุสมผลในขณะนี้ดังนั้นฉันจะตรวจสอบว่าสิ่งนี้แตกต่างไปจากเวอร์ชันคอมไพเลอร์เสถียรล่าสุดหรือไม่ )
chtz

11

ใช่รหัสในความเป็นจริงแบ่งใน C ++ 20

นิพจน์Foo{} != Foo{}มีสามตัวเลือกใน C ++ 20 (ในขณะที่มีเพียงหนึ่งใน 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

นี้มาจากใหม่เขียนใหม่ผู้สมัครกฎใน[over.match.oper] /3.4 ทั้งหมดของผู้สมัครเหล่านี้จะทำงานได้เนื่องจากเรามีปากเสียงไม่ได้Foo constในการหาผู้สมัครที่มีศักยภาพที่ดีที่สุดเราต้องผ่านผู้เข้าร่วมของเรา

กฎที่เกี่ยวข้องสำหรับฟังก์ชันที่ทำงานได้ดีที่สุดคือจาก[over.match.best] / 2 :

ป.ร. ให้คำนิยามเหล่านี้เป็นฟังก์ชั่นการทำงานได้F1ถูกกำหนดให้เป็นฟังก์ชั่นที่ดีกว่าฟังก์ชั่นการทำงานได้อีกF2ถ้าขัดแย้งทั้งหมดi, ไม่ได้เป็นลำดับแปลงเลวร้ายยิ่งกว่าแล้ว ICSi(F1)ICSi(F2)

  • [... กรณีที่ไม่เกี่ยวข้องจำนวนมากสำหรับตัวอย่างนี้ ... ] หรือหากไม่ใช่อย่างนั้น
  • F2 เป็นตัวเลือก rewritten ([over.match.oper]) และ F1 ไม่ใช่
  • F1 และ F2 เป็นตัวเลือกการเขียนใหม่และ F2 เป็นตัวสังเคราะห์ที่มีคำสั่งกลับด้านของพารามิเตอร์และ F1 ไม่ใช่

#2และ#3เป็นผู้สมัครเขียนใหม่และ#3มีการเรียงลำดับของพารามิเตอร์ในขณะที่#1ไม่ได้เขียนใหม่ แต่เพื่อที่จะไปที่ tiebreaker นั้นเราต้องผ่านเงื่อนไขเริ่มต้นก่อน: สำหรับการโต้แย้งทั้งหมดลำดับการแปลงนั้นไม่ได้เลวร้ายลง

#1ดีกว่า#2เพราะลำดับการแปลงทั้งหมดเหมือนกัน (เล็กน้อยเพราะพารามิเตอร์ฟังก์ชั่นเหมือนกัน) และ#2เป็นตัวเลือกที่เขียนใหม่ในขณะที่#1ไม่อยู่

แต่ ... ทั้งคู่#1/ #3และ#2/ #3 ติดอยู่กับเงื่อนไขแรกนั้น ในทั้งสองกรณีพารามิเตอร์แรกมีลำดับการแปลงที่ดีกว่าสำหรับ#1/ #2ในขณะที่พารามิเตอร์ที่สองมีลำดับการแปลงที่ดีกว่าสำหรับ#3(พารามิเตอร์ที่constต้องผ่านการconstรับรองพิเศษดังนั้นจึงมีลำดับการแปลงที่แย่กว่า) นี้constพลิกความล้มเหลวทำให้เราไม่สามารถที่จะต้องการคนใดคนหนึ่ง

เป็นผลให้ความละเอียดเกินพิกัดทั้งหมดไม่ชัดเจน

เท่าที่ฉันเข้าใจนี่ใช้ได้ผลตราบใดที่ประเภทการคืนเป็นboolเท่านั้น

ไม่ถูกต้อง เราพิจารณาผู้สมัครที่เขียนใหม่และกลับรายการโดยไม่มีเงื่อนไข กฎที่เรามีคือจาก[over.match.oper] / 9 :

หากoperator==ผู้สมัครที่ถูกเขียนซ้ำถูกเลือกโดยความละเอียดเกินพิกัดสำหรับผู้ประกอบ@การประเภทผลตอบแทนของมันจะเป็นcv bool

นั่นคือเรายังคงพิจารณาผู้สมัครเหล่านี้ แต่ถ้าผู้สมัครที่มีศักยภาพดีที่สุดคือการoperator==ส่งคืนก็ให้บอกว่าMeta- ผลลัพธ์นั้นเหมือนกับว่าผู้สมัครนั้นถูกลบ

เราไม่ต้องการที่จะอยู่ในสถานะที่การแก้ปัญหาการโอเวอร์โหลดจะต้องพิจารณาประเภทการส่งคืน และในกรณีใด ๆ ความจริงที่ว่ารหัสที่นี่ผลตอบแทนที่Metaเป็นสาระสำคัญ - boolปัญหานอกจากนี้ยังจะมีอยู่ถ้ามันกลับมา


โชคดีที่การแก้ไขที่นี่เป็นเรื่องง่าย:

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

เมื่อคุณสร้างโอเปอเรเตอร์การเปรียบเทียบconstแล้วจะไม่มีความคลุมเครืออีกต่อไป พารามิเตอร์ทั้งหมดเหมือนกันดังนั้นลำดับการแปลงทั้งหมดจึงเหมือนกันเล็กน้อย #1ตอนนี้จะเอาชนะ#3โดยไม่ได้เขียนใหม่และ#2ตอนนี้จะเอาชนะ#3โดยไม่ถูกย้อนกลับ - ซึ่งทำให้#1ผู้สมัครที่ทำงานได้ดีที่สุด ผลลัพธ์เดียวกับที่เรามีใน C ++ 17 เพียงไม่กี่ก้าวที่จะไปถึงที่นั่น


" เราไม่ต้องการที่จะอยู่ในสถานะที่มีความละเอียดเกินจะต้องพิจารณาประเภทการกลับมา. " เพียงเพื่อจะชัดเจนในขณะที่ความละเอียดเกินตัวเองไม่ได้พิจารณาประเภทผลตอบแทนการดำเนินงานตามมาเขียนใหม่ทำ รหัสหนึ่งคือไม่ดีที่เกิดขึ้นถ้าความละเอียดเกินจะเลือกเขียนใหม่และประเภทของการกลับมาของฟังก์ชั่นที่เลือกไม่ได้== boolแต่การเลือกสรรนี้จะไม่เกิดขึ้นในระหว่างการแก้ปัญหาการโอเวอร์โหลดตัวเอง
Nicol Bolas

จริง ๆ แล้วมันเป็นรูปแบบที่ไม่ดีเท่านั้นหากประเภทการส่งคืนเป็นสิ่งที่ไม่สนับสนุนโอเปอเรเตอร์! ...
Chris Dodd

1
@ChrisDodd ไม่มันจะต้องเป็นอย่างแน่นอนcv bool(และก่อนการเปลี่ยนแปลงนี้ความต้องการคือการแปลงตามบริบทเป็นbool- ยังไม่!)
Barry

น่าเสียดายที่นี่ไม่ได้แก้ปัญหาที่เกิดขึ้นจริงของฉัน แต่นั่นเป็นเพราะฉันไม่ได้ให้ MRE ซึ่งอธิบายถึงปัญหาของฉัน ฉันจะยอมรับสิ่งนี้และเมื่อฉันสามารถลดปัญหาของฉันได้อย่างถูกต้องฉันจะถามคำถามใหม่ ...
chtz

2
ดูเหมือนว่าการลดที่เหมาะสมสำหรับปัญหาดั้งเดิมคือgcc.godbolt.org/z/tFy4qz
TC

5

[over.match.best] / 2 แสดงว่าโอเวอร์โหลดที่ถูกต้องในชุดถูกจัดลำดับความสำคัญอย่างไร มาตรา2.8บอกเราว่าF1จะดีกว่าF2ถ้า (ในหลาย ๆสิ่งอื่น ๆ ):

F2เป็นตัวเลือก rewritten ([over.match.oper]) และF1ไม่ใช่

ตัวอย่างที่แสดงให้เห็นชัดเจนoperator<ว่าถูกเรียกแม้ว่าoperator<=>จะมี

และ[over.match.oper] /3.4.3บอกเราว่าผู้สมัครรับเลือกตั้งoperator==ในกรณีนี้เป็นผู้สมัครเขียนใหม่

อย่างไรก็ตามผู้ให้บริการของคุณลืมสิ่งสำคัญอย่างหนึ่ง: พวกเขาควรจะconstทำหน้าที่ และทำให้พวกเขาไม่ได้constทำให้แง่มุมของความละเอียดเกินพิกัดก่อนหน้านี้เข้ามาเล่น ฟังก์ชั่นไม่เป็นตรงตามที่ไม่ใช่const-to- constแปลงต้องเกิดขึ้นสำหรับข้อโต้แย้งที่แตกต่างกัน นั่นทำให้เกิดความกำกวมในคำถาม

เมื่อคุณทำให้พวกเขาconst, เสียงดังกราวลำคอมไพล์

ฉันไม่สามารถพูดกับส่วนที่เหลือของ Eigen ได้เนื่องจากฉันไม่รู้รหัสมันมีขนาดใหญ่มากและไม่เหมาะกับ MCVE


2
เราไปที่ tiebreaker ที่คุณระบุไว้หากมีการแปลงที่ดีเท่ากันทุกข้อโต้แย้ง แต่ไม่มี: เนื่องจากconstผู้สมัครที่ไม่กลับรายการมีลำดับการแปลงที่ดีกว่าสำหรับอาร์กิวเมนต์ที่สองและผู้สมัครที่กลับรายการมีลำดับการแปลงที่ดีกว่าสำหรับอาร์กิวเมนต์แรก
ริชาร์ดสมิ ธ

@RichardSmith: ใช่นั่นคือความซับซ้อนที่ฉันพูดถึง แต่ผมไม่ได้ต้องการให้มีการจริงไปถึงและอ่าน / internalize กฎเหล่านั้น;)
โรล Bolas

แน่นอนฉันลืมconstตัวอย่างเล็ก ๆ น้อย ๆ ฉันค่อนข้างแน่ใจว่า Eigen ใช้constทุกที่ (หรือคำจำกัดความของคลาสนอกพร้อมกับconstการอ้างอิง) แต่ฉันต้องตรวจสอบ ฉันพยายามที่จะแยกแยะกลไกโดยรวมที่ Eigen ใช้เป็นตัวอย่างเล็ก ๆ เมื่อฉันหาเวลา
chtz

-1

เรามีปัญหาที่คล้ายกันกับไฟล์ส่วนหัวของ Goopax ของเรา รวบรวมข้อมูลต่อไปนี้ด้วย clang-10 และ -std = c ++ 2a สร้างข้อผิดพลาดของคอมไพเลอร์

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

การให้ตัวดำเนินการเพิ่มเติมเหล่านี้ดูเหมือนว่าจะแก้ปัญหาได้:

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
นี่ไม่ใช่สิ่งที่จะเป็นประโยชน์ที่จะทำไว้ล่วงหน้าหรือไม่? มิฉะนั้นจะa == 0รวบรวมได้อย่างไร
Nicol Bolas

นี่ไม่ใช่ปัญหาที่คล้ายกันจริงๆ ดังที่ Nicol ชี้ว่าสิ่งนี้ไม่ได้รวบรวมใน C ++ 17 มันยังไม่คอมไพล์ใน C ++ 20 เพียงเพื่อเหตุผลอื่น
Barry

ฉันลืมที่จะพูดถึง: เรายังให้บริการผู้ให้บริการสมาชิก ด้วยgpu_bool gpu_type<T>::operator==(T a) const;และgpu_bool gpu_type<T>::operator!=(T a) const;ด้วย C ++ - 17 มันใช้งานได้ดี แต่ตอนนี้กับเสียงดังกราว-10 และ C ++ - 20 boolเหล่านี้จะไม่พบอีกต่อไปและแทนที่จะคอมไพเลอร์พยายามที่จะสร้างผู้ประกอบการของตัวเองโดยการแลกเปลี่ยนข้อโต้แย้งและจะล้มเหลวเพราะประเภทกลับไม่ได้
Ingo Josopait
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.