มาตรฐาน C ++ อนุญาตให้บูลที่ไม่กำหนดค่าเริ่มต้นขัดข้องโปรแกรมหรือไม่


500

ฉันรู้ว่า"พฤติกรรมที่ไม่ได้กำหนด"ใน C ++ สามารถอนุญาตให้คอมไพเลอร์ทำสิ่งที่ต้องการได้ อย่างไรก็ตามฉันมีข้อผิดพลาดที่ทำให้ฉันประหลาดใจเพราะฉันคิดว่ารหัสนั้นปลอดภัยพอ

ในกรณีนี้ปัญหาจริงเกิดขึ้นเฉพาะแพลตฟอร์มที่ใช้คอมไพเลอร์เฉพาะและเฉพาะในกรณีที่เปิดใช้งานการเพิ่มประสิทธิภาพ

ฉันลองหลายสิ่งหลายอย่างเพื่อทำให้เกิดปัญหาอีกครั้งและทำให้มันง่ายขึ้นสูงสุด นี่คือการแยกฟังก์ชั่นที่เรียกSerializeว่าจะใช้พารามิเตอร์บูลและคัดลอกสตริงtrueหรือfalseไปยังบัฟเฟอร์ปลายทางที่มีอยู่

ฟังก์ชั่นนี้จะอยู่ในการตรวจสอบรหัสหรือไม่ไม่มีทางที่จะบอกได้ว่าในความเป็นจริงอาจมีปัญหาหากพารามิเตอร์ bool เป็นค่าเริ่มต้นหรือไม่

// Zero-filled global buffer of 16 characters
char destBuffer[16];

void Serialize(bool boolValue) {
    // Determine which string to print based on boolValue
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    const size_t len = strlen(whichString);

    // Copy string into destination buffer, which is zero-filled (thus already null-terminated)
    memcpy(destBuffer, whichString, len);
}

หากโค้ดนี้ถูกประมวลผลด้วยการเพิ่มประสิทธิภาพเสียงดังกังวาน 5.0.0 + มันจะ / สามารถล้มเหลว

ผู้ประกอบการที่คาดหวังจะboolValue ? "true" : "false"ดูปลอดภัยพอสำหรับฉันฉันสันนิษฐานว่า "สิ่งที่มีค่าขยะอยู่ในboolValueนั้นไม่สำคัญเพราะจะประเมินว่าเป็นจริงหรือเท็จ แต่อย่างใด"

ฉันได้ติดตั้งตัวอย่างคอมไพเลอร์ Explorerที่แสดงปัญหาในการถอดแยกชิ้นส่วนนี่คือตัวอย่างที่สมบูรณ์ หมายเหตุ: ในการทำซ้ำปัญหาชุดค่าผสมที่ฉันพบว่าใช้งานได้คือใช้ Clang 5.0.0 พร้อมการเพิ่มประสิทธิภาพ -O2

#include <iostream>
#include <cstring>

// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
    bool uninitializedBool;

   __attribute__ ((noinline))  // Note: the constructor must be declared noinline to trigger the problem
   FStruct() {};
};

char destBuffer[16];

// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
    // Determine which string to print depending if 'boolValue' is evaluated as true or false
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    size_t len = strlen(whichString);

    memcpy(destBuffer, whichString, len);
}

int main()
{
    // Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
    FStruct structInstance;

    // Output "true" or "false" to stdout
    Serialize(structInstance.uninitializedBool);
    return 0;
}

ปัญหาเกิดขึ้นเนื่องจากเครื่องมือเพิ่มประสิทธิภาพ: มันฉลาดพอที่จะอนุมานได้ว่าสตริง "จริง" และ "เท็จ" มีความยาวต่างกันเพียง 1 เท่านั้นดังนั้นแทนที่จะคำนวณความยาวจริง ๆ มันใช้ค่าของบูลเองซึ่งควรในทางเทคนิคจะเป็น 0 หรือ 1 และจะเป็นดังนี้:

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

ในขณะที่นี่คือ "ฉลาด" เพื่อพูดคำถามของฉันคือ: มาตรฐาน C ++ อนุญาตให้คอมไพเลอร์ที่จะถือว่าบูลสามารถมีตัวเลขแทนภายในของ '0' หรือ '1' และใช้ในลักษณะดังกล่าวหรือไม่?

หรือนี่เป็นกรณีของการนำไปใช้งานที่กำหนดไว้ซึ่งในกรณีนี้การนำไปใช้สันนิษฐานว่า bools ทั้งหมดของมันจะมี 0 หรือ 1 เท่านั้นและค่าอื่นใดคืออาณาเขตพฤติกรรมที่ไม่ได้กำหนด?


200
มันเป็นคำถามที่ยอดเยี่ยม มันเป็นตัวอย่างที่ชัดเจนว่าพฤติกรรมที่ไม่ได้กำหนดไม่ได้เป็นเพียงแค่ข้อกังวลทางทฤษฎี เมื่อมีคนพูดว่าอะไรก็ตามที่สามารถเกิดขึ้นได้จาก UB นั้น "อะไร" นั้นน่าแปลกใจทีเดียว หนึ่งอาจคิดว่าพฤติกรรมที่ไม่ได้กำหนดยังคงปรากฏในวิธีที่คาดเดาได้ แต่วันนี้ด้วยเครื่องมือเพิ่มประสิทธิภาพที่ทันสมัยที่ไม่เป็นความจริงเลย OP ใช้เวลาในการสร้าง MCVE ตรวจสอบปัญหาอย่างละเอียดตรวจสอบการถอดแยกชิ้นส่วนและถามคำถามที่ชัดเจนตรงไปตรงมา ไม่สามารถขออะไรเพิ่มเติม
John Kugelman

7
สังเกตว่าข้อกำหนดที่“ ไม่เป็นศูนย์ประเมินเป็นtrue” เป็นกฎเกี่ยวกับการดำเนินการบูลีนรวมถึง“ การมอบหมายให้บูล” (ซึ่งอาจจะเรียกโดยนัยว่าstatic_cast<bool>()ขึ้นอยู่กับเฉพาะ) อย่างไรก็ตามมันไม่ได้เป็นข้อกำหนดเกี่ยวกับการเป็นตัวแทนภายในของการboolคัดเลือกโดยคอมไพเลอร์
Euro Micelli

2
ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
ซามูเอล Liew

3
ในบันทึกที่เกี่ยวข้องมากนี่คือที่มา "สนุก" ของการเข้ากันไม่ได้ของไบนารี หากคุณมี ABI A ที่ค่าศูนย์แผ่นก่อนที่จะเรียกใช้ฟังก์ชั่น แต่รวบรวมฟังก์ชั่นเช่นว่ามันถือว่าพารามิเตอร์เป็นศูนย์เบาะและ ABI B ที่ตรงกันข้าม (ไม่เป็นศูนย์ แต่ไม่คิดศูนย์ - เบาะเสริม) มันส่วนใหญ่จะทำงาน แต่ฟังก์ชั่นการใช้ B ABI จะทำให้เกิดปัญหาถ้ามันเรียกฟังก์ชั่นการใช้ ABI ที่ใช้พารามิเตอร์ 'เล็ก' IIRC คุณมีสิ่งนี้ใน x86 พร้อมเสียงดังกราวและ ICC
TLW

1
@TLW: แม้ว่ามาตรฐานไม่จำเป็นต้องมีการใช้งานที่ให้วิธีการโทรหรือถูกเรียกโดยรหัสภายนอกมันจะเป็นประโยชน์ที่จะมีวิธีการระบุสิ่งต่าง ๆ สำหรับการใช้งานที่พวกเขามีความเกี่ยวข้อง (การใช้งานที่ไม่มีรายละเอียดดังกล่าว ที่เกี่ยวข้องสามารถละเว้นแอตทริบิวต์ดังกล่าว)
supercat

คำตอบ:


285

ใช่ ISO C ++ อนุญาตให้ใช้งาน (แต่ไม่ต้องการ) เพื่อเลือกตัวเลือกนี้

แต่โปรดทราบว่า ISO C ++ อนุญาตให้คอมไพเลอร์ปล่อยโค้ดที่ผิดพลาดตามวัตถุประสงค์ (เช่นคำสั่งที่ผิดกฎหมาย) หากโปรแกรมพบ UB เช่นเป็นวิธีที่จะช่วยคุณค้นหาข้อผิดพลาด (หรือเพราะเป็น DeathStation 9000 การปฏิบัติตามอย่างเคร่งครัดนั้นไม่เพียงพอสำหรับการใช้งาน C ++ เพื่อเป็นประโยชน์สำหรับวัตถุประสงค์ที่แท้จริง) ดังนั้น ISO C ++ จะอนุญาตให้คอมไพเลอร์สร้าง asm ที่ล้มเหลว (ด้วยเหตุผลที่ต่างกันโดยสิ้นเชิง) แม้ในรหัสที่คล้ายกันที่อ่านค่าuint32_tเริ่มต้น แม้ว่าสิ่งนั้นจะต้องเป็นประเภทเลย์เอาต์คงที่โดยไม่มีการดักจับ

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


คุณกำลังรวบรวมสำหรับx86-64 System V ABIซึ่งระบุว่าboolเป็นหาเรื่องฟังก์ชั่นในการลงทะเบียนเป็นตัวแทนจากบิตรูปแบบ- false=0และtrue=1ในต่ำ 8 บิตของการลงทะเบียน1 ในหน่วยความจำboolคือชนิด 1 ไบต์ที่ต้องมีค่าจำนวนเต็มเป็น 0 หรือ 1 อีกครั้ง

(ABI เป็นชุดของตัวเลือกการใช้งานที่คอมไพเลอร์สำหรับแพลตฟอร์มเดียวกันเห็นด้วยเพื่อให้พวกเขาสามารถสร้างรหัสที่เรียกใช้ฟังก์ชั่นซึ่งกันและกันรวมถึงขนาดประเภทกฎการวางเลย์เอาต์ของโครงสร้างและการประชุมที่เรียก)

ISO C ++ ไม่ได้ระบุ แต่การตัดสินใจ ABI เป็นที่แพร่หลายเพราะมันทำให้ bool-> int แปลงราคาถูก (ศูนย์เพียงส่วนขยาย) ฉันไม่ทราบ ABIs ใด ๆ ที่ไม่อนุญาตให้คอมไพเลอร์มีค่าเป็น 0 หรือ 1 สำหรับboolสถาปัตยกรรมใด ๆ (ไม่ใช่แค่ x86) จะช่วยเพิ่มประสิทธิภาพเช่น!myboolมีxor eax,1การพลิกบิตต่ำ: รหัสที่เป็นไปได้ใด ๆ ที่สามารถพลิกบิต / จำนวนเต็ม / บูลระหว่าง 0 และ 1 ในการเรียนการสอนของ หรือรวบรวมa&&bเป็นบิตและสำหรับboolประเภท คอมไพเลอร์บางตัวใช้ประโยชน์จากค่าบูลีนเป็น 8 บิตในคอมไพเลอร์ การดำเนินการกับพวกเขาไม่มีประสิทธิภาพหรือไม่ .

โดยทั่วไปกฎ as-if อนุญาตให้คอมไพเลอร์ใช้ประโยชน์จากสิ่งที่เป็นจริงบนแพลตฟอร์มเป้าหมายที่กำลังรวบรวมเพราะผลลัพธ์สุดท้ายจะเป็นโค้ดที่สามารถเรียกทำงานได้ซึ่งใช้ลักษณะการทำงานภายนอกที่มองเห็นได้เช่นเดียวกับซอร์ส C ++ (ด้วยข้อ จำกัด ทั้งหมดที่ไม่ได้กำหนดพฤติกรรมที่วางอยู่บนสิ่งที่เป็นจริง "มองเห็นภายนอก": ไม่ได้กับดีบักเกอร์ แต่จากหัวข้ออื่นในโปรแกรม C ++ ที่มีรูปแบบถูกต้อง / ถูกกฎหมาย)

คอมไพเลอร์ที่ได้รับอนุญาตอย่างแน่นอนที่จะใช้ประโยชน์จากการรับประกัน ABI ในของรหัส-Gen และรหัสให้เหมือนที่คุณพบที่เพิ่มประสิทธิภาพในการstrlen(whichString)
5U - boolValue
(BTW การเพิ่มประสิทธิภาพนี้เป็นวิธีที่ชาญฉลาด แต่อาจขาดความชัดเจนเมื่อเทียบกับการแตกแขนงและอินไลน์memcpyเป็นร้านค้าของข้อมูลทันที2 )

หรือคอมไพเลอร์อาจสร้างตารางพอยน์เตอร์และทำดัชนีด้วยค่าจำนวนเต็มของbool, อีกครั้งโดยสมมติว่าเป็น 0 หรือ 1 ( ความเป็นไปได้นี้คือสิ่งที่คำตอบของ @ Barmar แนะนำ )


คุณคอนสตรัคด้วยการเพิ่มประสิทธิภาพทำงานนำไปสู่เสียงดังกราวเพียงแค่โหลดไบต์จากสแต็คที่จะใช้เป็น__attribute((noinline)) uninitializedBoolมันทำให้พื้นที่สำหรับวัตถุในmainด้วยpush rax(ซึ่งมีขนาดเล็กและด้วยเหตุผลต่าง ๆ เกี่ยวกับการเป็นที่มีประสิทธิภาพsub rsp, 8) ดังนั้นสิ่งที่เป็นขยะในอัลในการเข้าเป็นค่าที่มันใช้สำหรับmain นี่คือเหตุผลที่คุณจริงมีค่าที่ไม่ได้เป็นเพียงแค่uninitializedBool0

5U - random garbageสามารถห่อค่าที่ไม่ได้ลงนามจำนวนมากได้อย่างง่ายดายนำ memcpy ไปยังหน่วยความจำที่ไม่ได้แมป ปลายทางอยู่ในที่จัดเก็บข้อมูลแบบสแตติกไม่ใช่สแต็กดังนั้นคุณจึงไม่เขียนทับที่อยู่ผู้ส่งคืนหรือสิ่งของ


การใช้งานอื่น ๆ สามารถสร้างทางเลือกที่แตกต่างกันเช่นและfalse=0 true=any non-zero valueจากนั้นเสียงดังกราวอาจจะไม่ให้รหัสที่เกิดปัญหาสำหรับนี้อินสแตนซ์ที่เฉพาะเจาะจงของ UB (แต่มันจะยังคงได้รับอนุญาตหากต้องการ) ฉันไม่รู้เกี่ยวกับการใช้งานที่เลือกสิ่งอื่นใดที่ x86-64 ทำboolแต่มาตรฐาน C ++ อนุญาตให้มีหลายสิ่งที่ไม่มีใครทำหรืออยากทำ ฮาร์ดแวร์ที่ไม่เหมือนกับซีพียูปัจจุบัน

ISO c ++ boolใบมันไม่ได้ระบุสิ่งที่คุณจะพบว่าเมื่อคุณตรวจสอบหรือปรับเปลี่ยนการแสดงวัตถุของ (เช่นโดยmemcpyการboolเข้าไปในunsigned charซึ่งคุณได้รับอนุญาตให้ทำเพราะchar*สามารถนามแฝงอะไรและunsigned charรับประกันว่าจะไม่มีบิตแพ็ดดังนั้นมาตรฐาน C ++ อย่างเป็นทางการช่วยให้คุณเป็นตัวแทนวัตถุ hexdump โดยไม่ต้อง UB ใด ๆ ชี้หล่อเพื่อคัดลอกวัตถุ การเป็นตัวแทนแตกต่างจากการกำหนดchar foo = my_boolแน่นอนดังนั้นการบูลีนถึง 0 หรือ 1 จะไม่เกิดขึ้นและคุณจะได้รับการแสดงวัตถุดิบ)

คุณได้บางส่วน "ซ่อน" UB noinlineบนเส้นทางการดำเนินการนี้จากคอมไพเลอร์ที่มี แม้ว่ามันจะไม่อินไลน์ แต่การเพิ่มประสิทธิภาพระหว่างโพรซีเดอร์ยังสามารถสร้างเวอร์ชันของฟังก์ชันที่ขึ้นอยู่กับนิยามของฟังก์ชันอื่น (ขั้นแรกเสียงดังกราวกำลังทำให้เรียกใช้งานได้ไม่ใช่ไลบรารีแบบแบ่งใช้ของ Unix ที่สามารถเกิดการแทรกสอดของสัญลักษณ์ได้ประการที่สองความหมายภายในclass{}คำจำกัดความดังนั้นหน่วยการแปลทั้งหมดจะต้องมีคำจำกัดความเหมือนinlineกัน

ดังนั้นคอมไพเลอร์สามารถเปล่งเพียงretหรือud2(คำสั่งที่ผิดกฎหมาย) เป็นคำนิยามสำหรับmainเพราะเส้นทางของการดำเนินการเริ่มต้นที่ด้านบนของการmainเผชิญหน้าพฤติกรรมที่ไม่ได้กำหนดอย่างหลีกเลี่ยงไม่ได้ (ซึ่งคอมไพเลอร์สามารถดูเวลาคอมไพล์ได้หากตัดสินใจติดตามพา ธ ผ่านคอนสตรัคเตอร์ที่ไม่ใช่แบบอินไลน์)

โปรแกรมใด ๆ ที่พบ UB นั้นไม่ได้ถูกกำหนดอย่างสมบูรณ์สำหรับการมีอยู่ทั้งหมด แต่ UB ภายในฟังก์ชั่นหรือif()สาขาที่ไม่เคยทำงานจริง ๆ จะไม่ทำให้โปรแกรมที่เหลือเสียหาย ในทางปฏิบัติซึ่งหมายความว่าคอมไพเลอร์สามารถตัดสินใจที่จะปล่อยคำสั่งที่ผิดกฎหมายหรือ a retหรือไม่ปล่อยสิ่งใดและตกอยู่ในบล็อก / ฟังก์ชั่นถัดไปสำหรับบล็อกพื้นฐานทั้งหมดที่สามารถพิสูจน์ได้ในเวลารวบรวมเพื่อนำไปสู่

ในทางปฏิบัติแล้วGCC และ Clang นั้นบางครั้งจะปล่อยud2UB แทนที่จะพยายามสร้างรหัสสำหรับเส้นทางของการดำเนินการที่ไม่มีเหตุผล หรือสำหรับกรณีเช่นการล้มจุดสิ้นสุดของvoidฟังก์ชันที่ไม่ใช่ฟังก์ชันบางครั้ง gcc จะละเว้นretคำสั่ง หากคุณคิดว่า "ฟังก์ชั่นของฉันจะกลับมาพร้อมกับขยะที่อยู่ใน RAX" คุณเข้าใจผิดอย่างมาก คอมไพเลอร์ C ++ สมัยใหม่ไม่รักษาภาษาเหมือนภาษาแอสเซมบลีแบบพกพาอีกต่อไป โปรแกรมของคุณจะต้องเป็นภาษา C ++ ที่ถูกต้องโดยไม่มีการตั้งสมมติฐานว่าฟังก์ชันของคุณในเวอร์ชันสแตนด์อะโลนอาจไม่ได้อยู่ในรูปแบบเดียว

อีกตัวอย่างที่สนุกคือเหตุใดการเข้าถึงหน่วยความจำ mmap'ed แบบไม่กำหนดแนวบางครั้งจึงแยก segfault บน AMD64 . x86 ไม่ผิดกับจำนวนเต็มที่ไม่ได้จัดใช่ไหม? เหตุใดจึงมีการจัดแนวที่uint16_t*ไม่ตรงเป็นปัญหา เพราะalignof(uint16_t) == 2และการละเมิดสมมติฐานนั้นนำไปสู่ ​​segfault เมื่อ auto-vectorizing กับ SSE2

ดู สิ่งที่โปรแกรมเมอร์ C ทุกคนควรรู้เกี่ยวกับพฤติกรรมที่ไม่ได้กำหนด # 1/3ซึ่งเป็นบทความโดยนักพัฒนาเสียงดังกราว

จุดสำคัญ: ถ้าคอมไพเลอร์สังเกตเห็น UB ที่รวบรวมเวลาก็อาจจะ "หยุด" (ปล่อย asm น่าแปลกใจ) เส้นทางรหัสผ่านของคุณที่สาเหตุ UB แม้ว่ากำหนดเป้าหมาย ABI ใด ๆ boolที่บิตรูปแบบเป็นตัวแทนวัตถุที่ถูกต้องสำหรับ

คาดหวังความเป็นศัตรูโดยรวมต่อความผิดพลาดจำนวนมากโดยโปรแกรมเมอร์โดยเฉพาะสิ่งที่คอมไพเลอร์สมัยใหม่เตือน นี่คือเหตุผลที่คุณควรใช้-Wallและแก้ไขคำเตือน C ++ ไม่ใช่ภาษาที่ใช้ง่ายและบางสิ่งใน C ++ อาจไม่ปลอดภัยแม้ว่ามันจะปลอดภัยใน asm บนเป้าหมายที่คุณกำลังรวบรวม (เช่นล้นลงนามคือ UB ใน C ++ และคอมไพเลอร์จะคิดว่ามันจะไม่เกิดขึ้นแม้ว่าจะรวบรวมคอมไพล์ของ x86 2 ตัวยกเว้นว่าคุณใช้clang/gcc -fwrapv)

การคอมไพล์เวลาที่เห็นได้ของ UB นั้นอันตรายเสมอและเป็นเรื่องยากที่จะแน่ใจ (ด้วยการเพิ่มประสิทธิภาพลิงค์เวลา) ว่าคุณได้ซ่อน UB จากคอมไพเลอร์จริงๆและสามารถให้เหตุผลเกี่ยวกับชนิดของ asm ที่จะสร้าง

ไม่น่าตื่นเต้นเกินไป บ่อยครั้งที่ผู้คอมไพล์เลอร์จะปล่อยให้คุณหนีไปกับบางสิ่งและปล่อยโค้ดอย่างที่คุณคาดหวังแม้จะเป็น UB ก็ตาม แต่บางทีมันอาจจะเป็นปัญหาในอนาคตหากคอมไพเลอร์ devs ใช้การเพิ่มประสิทธิภาพบางอย่างที่ได้รับข้อมูลเพิ่มเติมเกี่ยวกับช่วงค่า (เช่นตัวแปรไม่เป็นลบอาจอนุญาตให้ปรับการขยายสัญญาณให้เป็นศูนย์ฟรีส่วนขยายบน x86- 64) ตัวอย่างเช่นใน gcc และ clang ปัจจุบันการทำtmp = a+INT_MINไม่ได้ปรับให้เหมาะสมa<0เสมอ - เท็จเท่านั้นที่tmpเป็นลบเสมอ (เนื่องจากINT_MIN+ a=INT_MAXเป็นค่าลบสำหรับเป้าหมายที่สมบูรณ์ของ 2 นี้และaไม่สามารถสูงกว่านั้นได้อีก)

ดังนั้น gcc / เสียงดังกราวยังไม่เปลี่ยนใจไปยังข้อมูลช่วงการสืบทอดมาสำหรับปัจจัยการผลิตของการคำนวณเพียงผลอยู่บนสมมติฐานไม่ล้นลงนาม: ตัวอย่าง Godbolt ฉันไม่รู้ว่านี่เป็นการเพิ่มประสิทธิภาพหรือไม่โดยเจตนา "พลาด" ในชื่อที่เป็นมิตรกับผู้ใช้หรืออะไร

นอกจากนี้ทราบว่าการใช้งาน (aka คอมไพเลอร์) ที่ได้รับอนุญาตในการกำหนดพฤติกรรมที่ ISO c ++ ใบไม่ได้กำหนด ตัวอย่างเช่นคอมไพเลอร์ทั้งหมดที่รองรับ Intrinsics ของ Intel (เช่น_mm_add_ps(__m128, __m128)การปรับเวกเตอร์ SIMD ด้วยตนเอง) จะต้องอนุญาตให้สร้างพอยน์เตอร์ที่จัดแนวผิดซึ่งเป็น UB ใน C ++ แม้ว่าคุณจะไม่ตรวจสอบก็ตาม __m128i _mm_loadu_si128(const __m128i *)ไม่โหลด unaligned โดยการ misaligned __m128i*หาเรื่องไม่ได้หรือ void* `reinterpret_cast` กำลังอยู่ระหว่างตัวชี้เวกเตอร์ฮาร์ดแวร์และประเภทที่เกี่ยวข้องนั้นเป็นพฤติกรรมที่ไม่ได้กำหนดหรือไม่?char*

GNU C / C ++ ยังกำหนดพฤติกรรมของการเปลี่ยนหมายเลขเซ็นชื่อเชิงลบ (ซ้ายโดยไม่มี-fwrapv) แยกจากกฎ UB ที่ลงชื่อโดยทั่วไปของโอเวอร์โฟลว์ ( นี่คือ UB ใน ISO C ++ในขณะที่การเลื่อนด้านขวาของหมายเลขที่ลงชื่อมีการกำหนดการใช้งาน (ตรรกะเทียบกับเลขคณิต) การใช้งานที่มีคุณภาพดีเลือกเลขคณิตบน HW ที่มีการเลื่อนด้านขวาทางคณิตศาสตร์ แต่ ISO C ++ ไม่ได้ระบุ) สิ่งนี้ได้รับการบันทึกไว้ในหมวด Integer ของคู่มือ GCCพร้อมกับการกำหนดพฤติกรรมที่กำหนดโดยการนำไปปฏิบัติซึ่งมาตรฐาน C ต้องการการนำไปใช้เพื่อกำหนดวิธีใดวิธีหนึ่ง

มีปัญหาเรื่องคุณภาพของการติดตั้งที่นักพัฒนาคอมไพเลอร์ใส่ใจ โดยทั่วไปแล้วพวกเขาไม่ได้พยายามรวบรวมคอมไพเลอร์ที่เป็นศัตรูโดยเจตนา แต่การใช้ประโยชน์จากหลุมบ่อ UB ทั้งหมดใน C ++ (ยกเว้นที่พวกเขาเลือกที่จะกำหนด) เพื่อเพิ่มประสิทธิภาพที่ดีขึ้นสามารถแยกความแตกต่างได้ในบางครั้ง


เชิงอรรถ 1 : 56 บิตด้านบนอาจเป็นขยะที่ผู้ใช้ต้องละเว้นตามปกติสำหรับชนิดที่แคบกว่าการลงทะเบียน

( ABIs อื่น ๆทำให้ทางเลือกที่แตกต่างกันที่นี่ . บางคนไม่จำเป็นต้องมีประเภทจำนวนเต็มแคบจะเป็น zero- หรือลงชื่อเข้าใช้ขยายการกรอกข้อมูลลงทะเบียนเมื่อผ่านไปหรือกลับจากฟังก์ชั่นเช่น MIPS64 และ PowerPC64. ดูส่วนสุดท้ายของคำตอบ x86-64 นี้ ซึ่งเปรียบเทียบกับ ISAs ก่อนหน้านี้ )

ยกตัวอย่างเช่นการโทรอาจมีการคำนวณa & 0x01010101ใน RDI bool_func(a&1)และใช้มันอย่างอื่นก่อนที่จะเรียก ผู้เรียกสามารถปรับให้เหมาะที่สุด&1เพราะมันทำไปแล้วที่ไบต์ต่ำซึ่งเป็นส่วนหนึ่งของand edi, 0x01010101และมันรู้ว่าจำเป็นต้องมี callee เพื่อละเว้นไบต์สูง

หรือถ้าบูลถูกส่งเป็น ARG ครั้งที่ 3 ผู้โทรอาจปรับให้เหมาะกับขนาดโค้ดโหลดmov dl, [mem]แทนmovzx edx, [mem]โดยให้บันทึก 1 ไบต์ด้วยค่าใช้จ่ายของการพึ่งพาที่ผิดพลาดกับค่าเก่าของ RDX (หรือเอฟเฟกต์ลงทะเบียนบางส่วนขึ้นอยู่กับ บนรุ่น CPU) หรือหาเรื่องแรกmov dil, byte [r10]แทนที่จะเป็นmovzx edi, byte [r10]เพราะทั้งคู่ต้องการคำนำหน้า REX อยู่ดี

นี่คือเหตุผลที่ส่งเสียงดังกราวmovzx eax, dilในแทนSerialize sub eax, edi(สำหรับ args จำนวนเต็มเสียงดังกราวละเมิดกฎ ABI นี้ขึ้นอยู่กับพฤติกรรมที่ไม่มีเอกสารของ gcc และเสียงดังกราวเป็นศูนย์ - หรือจำนวนเต็มขยายแคบลงถึง 32 บิต เป็นสัญญาณหรือส่วนขยายศูนย์ที่จำเป็นเมื่อเพิ่ม 32 บิตออฟเซ็ต x86-64 ABI หรือไม่ ดังนั้นฉันสนใจที่จะเห็นว่ามันไม่ได้ทำสิ่งเดียวกันbool)


เชิงอรรถ 2: หลังจากการแยกคุณจะมีร้านค้าขนาด 4 ไบต์ - movกลางหรือ 4 ไบต์ + 1 ไบต์ ความยาวมีความหมายในความกว้างของร้านค้า + ออฟเซ็ต

OTOH, glibc memcpy จะทำการโหลดขนาด 4 ไบต์ / สองร้านโดยมีการทับซ้อนกันซึ่งขึ้นอยู่กับความยาวดังนั้นนี่จะทำให้สิ่งทั้งหมดปราศจากกิ่งที่มีเงื่อนไขบนบูลีน ดูL(between_4_7):บล็อกใน memcpy / memmove ของ glibc หรืออย่างน้อยก็ไปในทางเดียวกันสำหรับบูลีนในการแยกของ memcpy เพื่อเลือกขนาดของก้อน

หากอินไลน์คุณสามารถใช้ 2x mov-immediate + cmovและ offset ตามเงื่อนไขหรือคุณอาจปล่อยให้ข้อมูลสตริงอยู่ในหน่วยความจำ

หรือหากการปรับแต่งสำหรับ Intel Ice Lake ( ด้วยคุณสมบัติ Fast Short REP MOV ) ค่าจริงrep movsbอาจเหมาะสมที่สุด glibc memcpyอาจเริ่มใช้rep movsb งานขนาดเล็ก ๆ บน CPU ที่มีคุณสมบัติดังกล่าวช่วยประหยัดการแตกแขนงได้มากมาย


เครื่องมือสำหรับการตรวจจับ UB และการใช้ค่าที่ไม่ได้กำหนดค่าเริ่มต้น

ใน gcc และ clang คุณสามารถคอมไพล์ด้วย-fsanitize=undefinedเพื่อเพิ่ม instrumentation แบบรันไทม์ที่จะเตือนหรือเกิดข้อผิดพลาดกับ UB ที่เกิดขึ้นขณะรันไทม์ ที่จะไม่จับตัวแปรหน่วยแม้ว่า (เพราะจะไม่เพิ่มขนาดของประเภทเพื่อให้มีที่ว่างสำหรับบิต "ไม่กำหนดค่าเริ่มต้น")

ดูhttps://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/

ในการค้นหาการใช้งานข้อมูลที่ไม่ได้กำหนดค่าเริ่มต้นมี Address Sanitizer และ Memory Sanitizer ใน clang / LLVM https://github.com/google/sanitizers/wiki/MemorySanitizerจะแสดงตัวอย่างของclang -fsanitize=memory -fPIE -pieการตรวจจับการอ่านหน่วยความจำที่ไม่ได้เตรียมไว้ มันอาจจะทำงานได้ดีที่สุดถ้าคุณคอมไพล์โดยไม่มีการเพิ่มประสิทธิภาพดังนั้นการอ่านตัวแปรทั้งหมดจะจบลงด้วยการโหลดจากหน่วยความจำใน asm พวกเขาแสดงให้เห็นว่ามันถูกใช้-O2ในกรณีที่โหลดจะไม่เพิ่มประสิทธิภาพออกไป ฉันไม่ได้ลองเอง (ในบางกรณีเช่นไม่เริ่มต้นการสะสมก่อนที่จะสรุปอาร์เรย์เสียงดังกราว -O3 จะปล่อยรหัสที่รวมอยู่ในการลงทะเบียนแบบเวกเตอร์ที่ไม่ได้เริ่มต้นดังนั้นด้วยการเพิ่มประสิทธิภาพคุณสามารถมีกรณีที่ไม่มีหน่วยความจำที่อ่านเกี่ยวข้องกับ UB แต่-fsanitize=memory เปลี่ยน asm ที่สร้างขึ้นและอาจส่งผลให้เกิดการตรวจสอบเรื่องนี้)

มันจะทนต่อการคัดลอกของหน่วยความจำเริ่มต้นและตรรกะที่เรียบง่ายและการดำเนินการทางคณิตศาสตร์กับมัน โดยทั่วไป MemorySanitizer จะติดตามการแพร่กระจายของข้อมูลที่ไม่ได้กำหนดค่าเริ่มต้นในหน่วยความจำอย่างเงียบ ๆ และรายงานคำเตือนเมื่อมีการใช้รหัสสาขา (หรือไม่ถ่าย) ขึ้นอยู่กับค่าเริ่มต้น

MemorySanitizer ใช้ฟังก์ชั่นย่อยที่พบใน Valgrind (เครื่องมือ Memcheck)

มันควรจะทำงานสำหรับกรณีนี้เพราะการเรียกร้องให้ glibc memcpyมีlengthผลคำนวณได้จากหน่วยความจำจะเตรียม (ภายในห้องสมุด) lengthในสาขาซึ่งเป็นไปตาม หากมี inline รุ่นที่ไม่มีสาขาอย่างสมบูรณ์ที่เพิ่งใช้การcmovจัดทำดัชนีและร้านค้าสองแห่งมันอาจไม่ทำงาน

Valgrind'smemcheckจะค้นหาปัญหาประเภทนี้อีกครั้งโดยไม่บ่นว่าโปรแกรมคัดลอกข้อมูลที่ไม่มีการเตรียมข้อมูลเบื้องต้น แต่มันบอกว่ามันจะตรวจจับเมื่อ "การกระโดดหรือการย้ายแบบมีเงื่อนไขขึ้นอยู่กับค่าเริ่มต้น" เพื่อพยายามที่จะจับพฤติกรรมที่มองเห็นจากภายนอกซึ่งขึ้นอยู่กับข้อมูลที่ไม่มีการเริ่มต้น

บางทีความคิดที่อยู่เบื้องหลังการไม่ตั้งค่าสถานะเพียงโหลดคือ structs สามารถมีช่องว่างภายในและคัดลอกโครงสร้างทั้งหมด (รวมถึงช่องว่างภายใน) ด้วยโหลด / เก็บแบบกว้างเวกเตอร์ไม่ใช่ข้อผิดพลาดแม้ว่าสมาชิกแต่ละคนจะถูกเขียนทีละคนเท่านั้น ในระดับ asm ข้อมูลเกี่ยวกับสิ่งที่แพ็ดดิ้งและสิ่งที่เป็นส่วนหนึ่งของค่าได้สูญหายไป


2
ฉันเห็นกรณีที่แย่กว่านั้นที่ตัวแปรใช้ค่าไม่อยู่ในช่วงของจำนวนเต็ม 8 บิต แต่เฉพาะการลงทะเบียน CPU ทั้งหมด และ Itanium ก็มีตัวแปรที่แย่กว่านั้นการใช้ตัวแปร uninitialized สามารถทำให้เกิดปัญหาได้ทันที
Joshua

2
@ โจชัว: โอ้ใช่แล้วจุดที่ดีการเก็งกำไรอย่างชัดเจนของ Itanium จะติดแท็กค่าการลงทะเบียนที่มีจำนวน "ไม่เท่ากับ" เช่นนั้นโดยใช้ค่าความผิดพลาด
Peter Cordes

11
ยิ่งไปกว่านั้นนี่ยังแสดงให้เห็นว่าเหตุใด UB Featurebug ถูกนำมาใช้ในการออกแบบภาษา C และ C ++ ตั้งแต่แรก: เพราะมันให้คอมไพเลอร์อย่างอิสระเช่นนี้ซึ่งตอนนี้อนุญาตให้คอมไพเลอร์ที่ทันสมัยที่สุดทำการคุณภาพสูงเหล่านี้ การเพิ่มประสิทธิภาพที่ทำให้ C / C ++ ภาษาระดับกลางที่มีประสิทธิภาพสูงเช่นนั้น
The_Sympathizer

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

4
@The_Sympathizer: มีการรวม UB ไว้เพื่อให้การติดตั้งใช้งานไม่ว่าจะเป็นวิธีใดจะเป็นประโยชน์กับลูกค้ามากที่สุด มันไม่ได้ตั้งใจที่จะแนะนำว่าพฤติกรรมทั้งหมดควรได้รับการพิจารณาว่ามีประโยชน์เท่าเทียมกัน
supercat

56

คอมไพเลอร์ได้รับอนุญาตให้สมมติว่าค่าบูลีนที่ส่งเป็นอาร์กิวเมนต์เป็นค่าบูลีนที่ถูกต้อง (เช่นค่าที่ถูกกำหนดค่าเริ่มต้นหรือแปลงเป็นtrueหรือfalse) trueค่าไม่ได้ที่จะเป็นเช่นเดียวกับจำนวนเต็ม 1 - แน่นอนอาจจะมีการแสดงต่างๆของtrueและfalse- แต่พารามิเตอร์ต้องมีบางตัวแทนที่ถูกต้องของหนึ่งในสองคนนั้นค่าที่ "เป็นตัวแทนที่ถูกต้อง" เป็น implementation- ที่กำหนดไว้

ดังนั้นหากคุณไม่สามารถกำหนดค่าเริ่มต้น a boolหรือหากคุณประสบความสำเร็จในการเขียนทับมันผ่านตัวชี้บางประเภทที่แตกต่างกันสมมติฐานของคอมไพเลอร์จะผิดและพฤติกรรมที่ไม่ได้กำหนดจะเกิดขึ้น คุณได้รับคำเตือน:

50) การใช้ค่าบูลในวิธีที่อธิบายโดยมาตรฐานสากลนี้ว่า“ ไม่ได้กำหนด” เช่นโดยการตรวจสอบค่าของวัตถุอัตโนมัติที่ไม่มีการกำหนดค่าเริ่มต้นอาจทำให้มันทำงานเหมือนว่ามันไม่จริงหรือเท็จ (เชิงอรรถสำหรับ 6 6 of.9.1 ประเภทพื้นฐาน)


11
" trueค่าไม่จำเป็นต้องเหมือนกับเลขจำนวนเต็ม 1" ซึ่งเป็นประเภทที่ทำให้เข้าใจผิด แน่นอนว่ารูปแบบบิตที่เกิดขึ้นจริงอาจจะเป็นอย่างอื่น แต่เมื่อแปลงโดยปริยาย / การส่งเสริมการลงทุน (วิธีเดียวที่คุณจะได้เห็นเป็นค่าอื่นนอกเหนือtrue/ false) trueอยู่เสมอ1และfalse0อยู่เสมอ แน่นอนว่าคอมไพเลอร์ดังกล่าวจะไม่สามารถใช้เคล็ดลับที่คอมไพเลอร์นี้พยายามใช้ (โดยใช้boolรูปแบบบิตที่แท้จริงเท่านั้น0หรือ1) ดังนั้นจึงไม่เกี่ยวข้องกับปัญหาของ OP
ShadowRanger

4
@ShadowRanger คุณสามารถตรวจสอบการแทนวัตถุได้โดยตรง
TC

7
@shadowranger: ประเด็นของฉันคือการใช้งานอยู่ในความดูแล ถ้ามัน จำกัด การเป็นตัวแทนที่ถูกต้องของtrueรูปแบบบิต1นั่นคือสิทธิพิเศษของมัน หากมันเลือกชุดของการแสดงชุดอื่น ๆ ก็ไม่สามารถใช้การเพิ่มประสิทธิภาพที่ระบุไว้ที่นี่ หากมันเลือกตัวแทนเฉพาะนั้นก็สามารถ จะต้องมีความสอดคล้องภายในเท่านั้น คุณสามารถตรวจสอบการเป็นตัวแทนของboolโดยการคัดลอกลงในอาร์เรย์ไบต์; นั่นไม่ใช่ UB (แต่กำหนดตามการนำไปใช้)
38911

3
ใช่การเพิ่มประสิทธิภาพของคอมไพเลอร์ (คือโลกแห่งความจริง c ++ การดำเนินงาน) รหัสบ่อยบางครั้งจะปล่อยออกมาว่าขึ้นอยู่กับboolการมีบิตรูปแบบของหรือ0 1พวกเขาจะไม่บูลีนซ้ำboolทุกครั้งที่อ่านจากหน่วยความจำ (หรือการลงทะเบียนที่ถือฟังก์ชัน ARG) นั่นคือสิ่งที่คำตอบนี้จะพูด ตัวอย่าง : gcc4.7 + สามารถเพิ่มประสิทธิภาพreturn a||bในการor eax, ediในการทำงานกลับมาboolหรือ MSVC สามารถเพิ่มประสิทธิภาพในการa&b test cl, dlx86 ของtestเป็นค่าที่เหมาะสม andดังนั้นหากcl=1และธงชุดทดสอบตามdl=2 cl&dl = 0
Peter Cordes

5
ประเด็นเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดไว้คือคอมไพเลอร์ได้รับอนุญาตให้เขียนข้อสรุปเพิ่มเติมเกี่ยวกับเรื่องนี้เช่นสมมติว่าโค้ดพา ธ ซึ่งจะนำไปสู่การเข้าถึงค่าที่ไม่ได้กำหนดค่าเริ่มต้นนั้นจะไม่เกิดขึ้นเลย . ดังนั้นไม่ใช่แค่ความเป็นไปได้ที่ค่าระดับต่ำอาจแตกต่างจากศูนย์หรือหนึ่ง
Holger

52

ฟังก์ชันนั้นถูกต้อง แต่ในโปรแกรมทดสอบของคุณคำสั่งที่เรียกใช้ฟังก์ชันทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดโดยใช้ค่าของตัวแปรที่ไม่กำหนดค่าเริ่มต้น

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

พฤติกรรมที่ไม่ได้กำหนดหมายถึงสิ่งที่สามารถเกิดขึ้นได้ซึ่งรวมถึงโปรแกรมที่ขัดข้องไม่กี่บรรทัดหลังจากเหตุการณ์ที่ก่อให้เกิดพฤติกรรมที่ไม่ได้กำหนด

NB คำตอบของ "พฤติกรรมที่ไม่ได้กำหนดทำให้เกิด _____" อยู่เสมอ "ใช่" นั่นคือคำจำกัดความของพฤติกรรมที่ไม่ได้กำหนดอย่างแท้จริง


2
ประโยคแรกเป็นจริงหรือไม่? เป็นเพียงการคัดลอกbool UB ทริกเกอร์ที่ไม่ได้เตรียมการหรือไม่?
โจชัวกรี

10
@JoshuaGreen เห็น [dcl.init] / 12 "หากค่าที่ไม่แน่นอนถูกสร้างขึ้นโดยการประเมินผลพฤติกรรมจะไม่ได้กำหนดยกเว้นในกรณีต่อไปนี้:" (และไม่มีกรณีเหล่านี้มีข้อยกเว้นbool) การคัดลอกต้องการประเมินแหล่งที่มา
MM

8
@JoshuaGreen และสาเหตุที่คุณอาจมีแพลตฟอร์มที่ทำให้เกิดความผิดพลาดของฮาร์ดแวร์หากคุณเข้าถึงค่าที่ไม่ถูกต้องสำหรับบางประเภท บางครั้งเรียกว่า "การเป็นตัวแทนของแทร็บ"
David Schwartz

7
Itanium ในขณะที่คลุมเครือเป็นซีพียูที่ยังอยู่ในระหว่างการผลิตมีค่ากับดักและมีคอมไพเลอร์ C ++ อย่างน้อยสองรุ่นที่ทันสมัย ​​(Intel / HP) แท้จริงมันมีtrue, falseและnot-a-thingค่าบูลี
MSalters

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

23

บูลได้รับอนุญาตให้เก็บค่าขึ้นอยู่กับการใช้งานที่ใช้ภายในtrueและfalseและรหัสที่สร้างขึ้นสามารถสันนิษฐานได้ว่ามันจะเก็บค่าหนึ่งในสองค่าเหล่านี้เท่านั้น

โดยปกติแล้วการดำเนินการจะใช้จำนวนเต็ม0สำหรับfalseและ1สำหรับtrueเพื่อลดความซับซ้อนของการแปลงระหว่างboolและintและทำให้สร้างรหัสเดียวกับif (boolvar) if (intvar)ในกรณีนั้นเราสามารถจินตนาการได้ว่าโค้ดที่สร้างขึ้นสำหรับไตรภาคในการมอบหมายจะใช้ค่าเป็นดัชนีในอาร์เรย์พอยน์เตอร์ของพอยน์เตอร์สองสายคือมันอาจถูกแปลงเป็นดังนี้:

// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

หากไม่มีการกำหนดboolValueค่าเริ่มต้นมันสามารถเก็บค่าจำนวนเต็มใด ๆ ได้ซึ่งจะทำให้เกิดการเข้าถึงนอกขอบเขตของstringsอาร์เรย์


1
@SidS ขอบคุณ ในทางทฤษฎีการเป็นตัวแทนภายในอาจเป็นสิ่งที่ตรงกันข้ามกับวิธีที่พวกเขาส่งไปยัง / จากจำนวนเต็ม แต่นั่นจะเป็นสิ่งที่ผิดปกติ
Barmar

1
คุณพูดถูกและตัวอย่างของคุณก็จะผิดพลาด อย่างไรก็ตามมันเป็น "มองเห็นได้" สำหรับการตรวจสอบโค้ดที่คุณใช้ตัวแปรที่ไม่กำหนดค่าเริ่มต้นเป็นดัชนีของอาร์เรย์ นอกจากนี้ยังอาจมีปัญหาแม้ในการดีบั๊ก (ตัวอย่างเช่นดีบักเกอร์ / คอมไพเลอร์บางส่วนจะเริ่มต้นด้วยรูปแบบเฉพาะเพื่อให้ง่ายต่อการดูเมื่อเกิดปัญหา) ในตัวอย่างของฉันส่วนที่น่าประหลาดใจคือการใช้งานบูลมองไม่เห็น: เครื่องมือเพิ่มประสิทธิภาพตัดสินใจใช้ในการคำนวณที่ไม่ปรากฏในซอร์สโค้ด
Remz

3
@Remz ฉันแค่ใช้อาร์เรย์เพื่อแสดงว่าโค้ดที่สร้างขึ้นอาจเทียบเท่ากับอะไรโดยไม่แนะนำให้ใครก็ตามที่จะเขียนมัน
Barmar

1
@Remz หล่อboolไปintด้วย*(int *)&boolValueและพิมพ์สำหรับการแก้จุดบกพร่องเพื่อดูว่ามันเป็นสิ่งอื่นที่ไม่ใช่0หรือ1เมื่อเกิดปัญหา ถ้าเป็นกรณีนั้นมันค่อนข้างยืนยันทฤษฎีที่ว่าคอมไพเลอร์กำลังปรับอินไลน์ให้เหมาะสมถ้าเป็นอาเรย์ซึ่งอธิบายว่าทำไมมันถึงล้มเหลว
Havenard

2
@MSalters: std::bitset<8>ไม่ได้ให้ชื่อที่ดีแก่ฉันสำหรับธงที่แตกต่างของฉันทั้งหมด ซึ่งอาจมีความสำคัญ
Martin Bonner สนับสนุนโมนิก้า

15

การสรุปคำถามของคุณบ่อยครั้งคุณกำลังถามว่ามาตรฐาน C ++ อนุญาตให้คอมไพเลอร์สมมติว่าboolสามารถมีการแสดงตัวเลขภายในเป็น '0' หรือ '1' และใช้ในลักษณะดังกล่าวได้หรือไม่?

boolมาตรฐานกล่าวว่าไม่มีอะไรเกี่ยวกับการแสดงภายในของ มันจะกำหนดว่าจะเกิดอะไรขึ้นเมื่อทำการส่ง a boolไปยังint(หรือในทางกลับกัน) ส่วนใหญ่เนื่องจากการแปลงอินทิกรัลเหล่านี้ (และความจริงที่ว่าผู้คนพึ่งพาค่อนข้างมากกับพวกเขา) คอมไพเลอร์จะใช้ 0 และ 1 แต่ไม่จำเป็นต้อง (ถึงแม้ว่าจะต้องเคารพข้อ จำกัด ของ ABI ระดับต่ำกว่า) )

ดังนั้นคอมไพเลอร์เมื่อเห็น a boolมีสิทธิ์พิจารณาว่าที่กล่าวว่าboolมีรูปแบบบิต ' true' หรือ ' false' และทำทุกอย่างที่รู้สึก ดังนั้นถ้าค่าสำหรับtrueและfalseกำลัง 1 และ 0 ตามลำดับคอมไพเลอร์ที่ได้รับอนุญาตจริงเพื่อเพิ่มประสิทธิภาพในการstrlen 5 - <boolean value>พฤติกรรมความสนุกอื่น ๆ เป็นไปได้!

ดังที่ได้รับการระบุซ้ำ ๆ ที่นี่พฤติกรรมที่ไม่ได้กำหนดมีผลลัพธ์ที่ไม่ได้กำหนด รวมถึง แต่ไม่ จำกัด เพียง

  • รหัสของคุณทำงานตามที่คาดไว้
  • รหัสของคุณล้มเหลวในเวลาสุ่ม
  • รหัสของคุณไม่ได้ทำงานเลย

ดูสิ่งที่โปรแกรมเมอร์ทุกคนควรรู้เกี่ยวกับพฤติกรรมที่ไม่ได้กำหนด

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