ตัวนับเวลารวบรวม C ++ ได้รับการตรวจสอบอีกครั้ง


28

TL; DR

ก่อนที่คุณจะพยายามอ่านโพสต์นี้ให้รู้ว่า:

  1. พบวิธีแก้ไขปัญหาที่นำเสนอด้วยตัวเองแต่ฉันก็ยังอยากรู้ว่าการวิเคราะห์นั้นถูกต้องหรือไม่
  2. ฉันได้จัดทำแพคเกจลงในfameta::counterชั้นเรียนที่สามารถแก้ไขนิสัยใจคอที่เหลืออยู่สองสามข้อ คุณสามารถค้นหาได้ใน GitHub ;
  3. คุณสามารถเห็นมันในการทำงานใน godbolt

มันเริ่มต้นอย่างไร

ตั้งแต่ Filip Roséenค้นพบ / คิดค้นในปี 2015 เวทมนตร์ดำที่รวบรวมเวลาอยู่ใน C ++ฉันได้รับการหมกมุ่นกับอุปกรณ์อย่างอ่อนโยนดังนั้นเมื่อ CWG ตัดสินใจว่าการทำงานต้องไปฉันผิดหวัง แต่ก็ยังหวังว่าใจของพวกเขา อาจมีการเปลี่ยนแปลงโดยแสดงกรณีการใช้งานที่น่าสนใจ

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

มีการรายงานปัญหาบนเว็บไซต์ของRoséenและเมื่อเร็ว ๆ นี้บน stackoverflow: C ++ รองรับเคาน์เตอร์เวลาคอมไพล์หรือไม่?

ไม่กี่วันที่ผ่านมาฉันตัดสินใจลองแก้ไขปัญหาอีกครั้ง

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

ด้านล่างฉันกำลังนำเสนอรหัสดั้งเดิมของRoséenเพื่อความชัดเจน สำหรับคำอธิบายวิธีการใช้งานโปรดดูที่เว็บไซต์ของเขา :

template<int N>
struct flag {
  friend constexpr int adl_flag (flag<N>);
};

template<int N>
struct writer {
  friend constexpr int adl_flag (flag<N>) {
    return N;
  }

  static constexpr int value = N;
};

template<int N, int = adl_flag (flag<N> {})>
int constexpr reader (int, flag<N>) {
  return N;
}

template<int N>
int constexpr reader (float, flag<N>, int R = reader (0, flag<N-1> {})) {
  return R;
}

int constexpr reader (float, flag<0>) {
  return 0;
}

template<int N = 1>
int constexpr next (int R = writer<reader (0, flag<32> {}) + N>::value) {
  return R;
}

int main () {
  constexpr int a = next ();
  constexpr int b = next ();
  constexpr int c = next ();

  static_assert (a == 1 && b == a+1 && c == b+1, "try again");
}

ด้วยคอมไพเลอร์ g ++ และ clang ++ ล่าสุด-ish ให้next()ผลตอบแทนที่ 1 เสมอเมื่อทำการทดลองเล็กน้อยปัญหาอย่างน้อยกับ g ++ น่าจะเป็นที่เมื่อคอมไพเลอร์ประเมินค่าเริ่มต้นของฟังก์ชันเท็มเพลตพารามิเตอร์ในครั้งแรกที่เรียกฟังก์ชัน ฟังก์ชั่นเหล่านั้นไม่ได้ทำให้เกิดการประเมินค่าพารามิเตอร์เริ่มต้นอีกครั้งดังนั้นจึงไม่เคยทำให้ฟังก์ชั่นใหม่ ๆ เกิดขึ้นทันที


คำถามแรก

  1. คุณเห็นด้วยกับการวินิจฉัยของฉันนี้จริง ๆ ?
  2. ถ้าใช่พฤติกรรมใหม่นี้ได้รับคำสั่งจากมาตรฐานหรือไม่? ก่อนหน้านี้เป็นข้อผิดพลาดหรือไม่?
  3. ถ้าไม่เช่นนั้นปัญหาคืออะไร

เมื่อคำนึงถึงข้างต้นแล้วฉันก็คิดหาวิธีทำ: ทำเครื่องหมายการnext()ขอร้องแต่ละอันด้วยรหัสซ้ำที่เพิ่มขึ้นซ้ำซากจำเจเพื่อส่งต่อไปยัง callees เพื่อไม่ให้มีการโทรเหมือนกันดังนั้นบังคับให้คอมไพเลอร์ประเมินค่าอาร์กิวเมนต์ทั้งหมด แต่ละครั้ง.

ดูเหมือนว่าเป็นภาระที่จะทำเช่นนั้น แต่การคิดว่ามันสามารถใช้มาโครมาตรฐาน ( __LINE__หรือ__COUNTER__ที่ใดก็ได้) ที่ซ่อนอยู่ในcounter_next()แมโครที่เหมือนฟังก์ชั่น

ดังนั้นฉันจึงคิดสิ่งต่อไปนี้ซึ่งฉันนำเสนอในรูปแบบที่ง่ายที่สุดที่แสดงปัญหาที่ฉันจะพูดถึงในภายหลัง

template <int N>
struct slot;

template <int N>
struct slot {
    friend constexpr auto counter(slot<N>);
};

template <>
struct slot<0> {
    friend constexpr auto counter(slot<0>) {
        return 0;
    }
};

template <int N, int I>
struct writer {
    friend constexpr auto counter(slot<N>) {
        return I;
    }

    static constexpr int value = I-1;
};

template <int N, typename = decltype(counter(slot<N>()))>
constexpr int reader(int, slot<N>, int R = counter(slot<N>())) {
    return R;
};

template <int N>
constexpr int reader(float, slot<N>, int R = reader(0, slot<N-1>())) {
    return R;
};

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}

int a = next<11>();
int b = next<34>();
int c = next<57>();
int d = next<80>();

คุณสามารถสังเกตผลลัพธ์ของข้างบนในgodboltซึ่งฉันจับภาพหน้าจอสำหรับ lazies

ป้อนคำอธิบายรูปภาพที่นี่

และอย่างที่คุณเห็นด้วย trunk g ++ และ clang ++ จนถึง 7.0.0 มันใช้งานได้! ตัวนับเพิ่มจาก 0 เป็น 3 ตามที่คาดไว้ แต่ด้วยเวอร์ชันเสียงดังกราว ++ สูงกว่า 7.0.0 ไม่เป็นเช่นนั้น

หากต้องการเพิ่มการดูถูกการบาดเจ็บฉันได้พยายามทำให้เสียงดังกราว ++ เป็นเวอร์ชัน 7.0.0 พังโดยเพียงแค่เพิ่มพารามิเตอร์ "บริบท" ในการผสมเช่นที่เคาน์เตอร์จะผูกกับบริบทนั้นและเช่นสามารถ จะเริ่มต้นใหม่เมื่อใดก็ตามที่มีการกำหนดบริบทใหม่ซึ่งเปิดขึ้นเพื่อความเป็นไปได้ที่จะใช้จำนวนที่ไม่มีที่สิ้นสุดของเคาน์เตอร์ ด้วยตัวแปรนี้เสียงดังกราว ++ เหนือรุ่น 7.0.0 ไม่ผิดพลาด แต่ก็ยังไม่ได้ผลตามที่คาดหวัง อยู่บน godbolt

ที่สูญเสียของเบาะแสใด ๆ เกี่ยวกับสิ่งที่เกิดขึ้นผมได้ค้นพบcppinsights.ioเว็บไซต์หนึ่งที่ช่วยให้เห็นวิธีการและเมื่อแม่ได้รับ instantiated การใช้บริการนั้นสิ่งที่ฉันคิดว่าเกิดขึ้นคือเสียงดังกังวาน ++ ไม่ได้กำหนดfriend constexpr auto counter(slot<N>)ฟังก์ชั่นใด ๆ เมื่อใดก็ตามที่writer<N, I>มีอินสแตนซ์

ความพยายามที่จะเรียกcounter(slot<N>)หา N ใด ๆ ที่ควรได้รับอินสแตนซ์แล้วน่าจะเป็นพื้นฐานของสมมติฐานนี้

แต่ถ้าฉันพยายามที่จะชัดเจน instantiate writer<N, I>สำหรับการใด ๆ ที่ได้รับNและIที่ควรได้รับอยู่แล้ว instantiated แล้วเสียงดังกราว ++ friend constexpr auto counter(slot<N>)บ่นเกี่ยวกับนิยามใหม่

เพื่อทดสอบข้างต้นฉันได้เพิ่มอีกสองบรรทัดในซอร์สโค้ดก่อนหน้า

int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;

คุณสามารถเห็นมันทั้งหมดด้วยตัวคุณเองใน godbolt ภาพหน้าจอด้านล่าง

เสียงดังกังวาน ++ เชื่อว่ามันได้กำหนดสิ่งที่มันเชื่อว่ามันไม่ได้กำหนด

ดังนั้นดูเหมือนว่าเสียงดังกราว ++ เชื่อว่ามันได้กำหนดสิ่งที่มันเชื่อว่ามันไม่ได้นิยามไว้ซึ่งทำให้หัวของคุณหมุนได้ใช่ไหม?


คำถามชุดที่สอง

  1. วิธีแก้ปัญหาทางกฎหมายของฉันคือC ++ เลยหรือฉันจัดการเพื่อค้นพบข้อผิดพลาด g ++ อีกครั้งหรือไม่?
  2. ถ้ามันถูกกฎหมายผมก็เลยพบข้อผิดพลาดเกี่ยวกับเสียงดังกราว ++ ที่น่ารังเกียจบ้างไหม?
  3. หรือว่าฉันเพิ่งเจาะลึกเข้าไปในโลกมืดของพฤติกรรมที่ไม่ได้กำหนดดังนั้นฉันจึงเป็นคนเดียวที่จะตำหนิ?

ไม่ว่าจะในกรณีใดฉันยินดีต้อนรับทุกคนอย่างอบอุ่นที่ต้องการช่วยฉันออกจากโพรงกระต่ายนี้โดยอธิบายคำอธิบายอาการปวดศีรษะหากจำเป็น : D


2
ที่เกี่ยวข้อง: stackoverflow.com/questions/51601439/…
HolyBlackCat

2
ในขณะที่ฉันจำได้ว่าคนในคณะกรรมการมาตรฐานมีความตั้งใจที่ชัดเจนที่จะไม่อนุญาตให้มีการคอมไพล์เวลาโครงสร้างทุกรูปแบบหรือรูปแบบที่ไม่ให้ผลลัพธ์ที่เหมือนกันทุกครั้งที่พวกเขาได้รับการประเมิน ดังนั้นจึงอาจเป็นข้อผิดพลาดของคอมไพเลอร์มันอาจเป็นกรณี "ไม่ดีไม่จำเป็นต้องวินิจฉัย" หรืออาจเป็นสิ่งที่มาตรฐานพลาดไป อย่างไรก็ตามมันขัดกับ "จิตวิญญาณของมาตรฐาน" ฉันขอโทษ. ฉันจะชอบรวบรวมเวลาเคาน์เตอร์ด้วย
bolov

@HolyBlackCat ฉันต้องยอมรับว่าฉันพบว่ามันยากมากที่จะเอาหัวอ่านรหัสนั้น มันดูเหมือนว่าจะสามารถหลีกเลี่ยงความต้องการในการส่งจำนวนที่เพิ่มขึ้นอย่างซ้ำซากจำเจเป็นพารามิเตอร์ให้กับnext()ฟังก์ชั่น แต่ฉันไม่สามารถคิดออกได้ว่าวิธีการทำงาน ไม่ว่าในกรณีใดฉันจะได้รับการตอบสนองต่อปัญหาของฉันเองที่นี่: stackoverflow.com/a/60096865/566849
Fabio A.

@FabioA ฉันก็ไม่เข้าใจคำตอบทั้งหมดเช่นกัน ตั้งแต่ถามคำถามฉันรู้ว่าฉันไม่ต้องการสัมผัสตัวนับยอดอีกเลย
HolyBlackCat

ในขณะที่การทดลองใช้ความคิดเล็ก ๆ น้อย ๆ สนุก ๆ ใครบางคนที่ใช้รหัสนั้นจริง ๆ แล้วต้องคาดหวังว่ามันจะไม่ทำงานใน C ++ รุ่นต่อไปใช่ไหม ในแง่ที่ว่าผลลัพธ์ที่กำหนดตัวเองเป็นข้อผิดพลาด
Aziuth

คำตอบ:


5

หลังจากการตรวจสอบเพิ่มเติมปรากฎว่ามีการแก้ไขเล็กน้อยที่สามารถดำเนินการกับnext()ฟังก์ชันได้ซึ่งทำให้โค้ดทำงานอย่างถูกต้องในเวอร์ชัน clang ++ ด้านบน 7.0.0 แต่ทำให้หยุดทำงานสำหรับเวอร์ชัน clang ++ อื่น ๆ ทั้งหมด

ดูรหัสต่อไปนี้นำมาจากโซลูชันก่อนหน้าของฉัน

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}

หากคุณใส่ใจกับมันสิ่งที่มันอย่างแท้จริงก็คือการพยายามที่จะอ่านค่าที่เกี่ยวข้องกับการslot<N>เพิ่ม 1 ไปและจากนั้นเชื่อมโยงค่าใหม่นี้เพื่อเดียวกันมาก slot<N>

เมื่อslot<N>ไม่มีค่าที่เกี่ยวข้องค่าที่เกี่ยวข้องกับslot<Y>จะถูกดึงแทนด้วยYการเป็นดัชนีสูงสุดน้อยกว่าNเช่นนั้นที่slot<Y>มีค่าที่เกี่ยวข้อง

ปัญหาของโค้ดข้างต้นคือถึงแม้ว่ามันจะใช้งานได้ใน g ++, clang ++ (ใช่แล้วฉันจะบอกว่า?) ทำให้สิ่งที่มันคืนกลับมาreader(0, slot<N>()) อย่างถาวรเมื่อslot<N>ไม่มีค่าที่เกี่ยวข้อง 0ในทางกลับกันที่นี้หมายถึงว่าช่องทั้งหมดได้รับการเชื่อมโยงอย่างมีประสิทธิภาพด้วยค่าฐาน

ทางออกคือการแปลงรหัสข้างต้นเป็นหนึ่งนี้

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}

ขอให้สังเกตว่าได้รับการแก้ไขลงในslot<N>() slot<N-1>()มันสมเหตุสมผล: ถ้าฉันต้องการเชื่อมโยงค่ากับslot<N>มันหมายความว่ายังไม่มีการเชื่อมโยงค่าดังนั้นจึงไม่มีเหตุผลที่จะพยายามเรียกคืน นอกจากนี้เรายังต้องการที่จะเพิ่มเคาน์เตอร์และความคุ้มค่าของเคาน์เตอร์ที่เกี่ยวข้องกับการจะต้องมีหนึ่งบวกค่าที่เกี่ยวข้องกับslot<N>slot<N-1>

ยูเรก้า!

แม้ว่าจะแบ่งเวอร์ชันเสียงดังกราว ++ <= 7.0.0

สรุปผลการวิจัย

สำหรับฉันดูเหมือนว่าโซลูชันดั้งเดิมที่ฉันโพสต์มีข้อบกพร่องทางแนวคิดเช่นนั้น:

  • g ++ มีการเล่นโวหาร / ข้อผิดพลาด / การพักผ่อนที่ยกเลิกกับข้อผิดพลาดของโซลูชันของฉันและในที่สุดก็ทำให้รหัสทำงานอย่างไรก็ตาม
  • clang ++ เวอร์ชั่น> 7.0.0 นั้นเข้มงวดกว่าและไม่ชอบข้อบกพร่องในรหัสต้นฉบับ
  • clang ++ เวอร์ชัน <= 7.0.0 มีข้อบกพร่องที่ทำให้การแก้ไขที่แก้ไขไม่ทำงาน

ยิ่งไปกว่านั้นโค้ดต่อไปนี้ใช้ได้กับ g ++ และ clang ++ ทุกเวอร์ชัน

#if !defined(__clang_major__) || __clang_major__ > 7
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}
#else
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}
#endif

รหัสตาม - ยังใช้งานได้กับ msvc คอมไพเลอร์ ICC ไม่เรียก SFINAE เมื่อใช้decltype(counter(slot<N>()))เลือกที่จะบ่นเกี่ยวกับการไม่สามารถที่จะเพราะdeduce the return type of function "counter(slot<N>)" it has not been definedผมเชื่อว่าปัญหานี้เป็นปัญหาที่สามารถทำงานรอบด้วยการทำ SFINAE counter(slot<N>)บนผลโดยตรงจาก วิธีนี้ใช้ได้กับคอมไพเลอร์อื่น ๆ ด้วยเช่นกัน แต่ g ++ ตัดสินใจที่จะคายคำเตือนที่น่ารำคาญจำนวนมากซึ่งไม่สามารถปิดได้ ดังนั้นในกรณีนี้#ifdefก็สามารถช่วยได้

หลักฐานอยู่ใน godbolt , screnshotted ด้านล่าง

ป้อนคำอธิบายรูปภาพที่นี่


2
ฉันคิดว่าการตอบแบบนี้เป็นการปิดหัวข้อ แต่ฉันก็ยังอยากรู้ว่าฉันถูกต้องในการวิเคราะห์หรือไม่ดังนั้นฉันจะรอก่อนที่จะยอมรับคำตอบของฉันถูกต้องหวังว่าคนอื่นจะผ่านและให้เบาะแสที่ดีกว่า หรือการยืนยัน :)
Fabio A.
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.