สาขาที่มีพฤติกรรมที่ไม่ได้กำหนดสามารถถือว่าไม่สามารถเข้าถึงได้และปรับให้เหมาะสมเป็นรหัสตายได้หรือไม่


89

พิจารณาข้อความต่อไปนี้:

*((char*)NULL) = 0; //undefined behavior

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

โปรแกรมต่อไปนี้จะถูกกำหนดไว้อย่างดีในกรณีที่ผู้ใช้ไม่เคยป้อนหมายเลข3หรือไม่?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

หรือเป็นพฤติกรรมที่ไม่ได้กำหนดโดยสิ้นเชิงไม่ว่าผู้ใช้จะเข้ามา?

นอกจากนี้คอมไพเลอร์สามารถสันนิษฐานได้ว่าพฤติกรรมที่ไม่ได้กำหนดจะไม่ถูกดำเนินการในรันไทม์? ซึ่งจะช่วยให้สามารถใช้เหตุผลย้อนหลังได้:

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

ที่นี่คอมไพเลอร์อาจให้เหตุผลว่าในกรณีที่num == 3เรามักจะเรียกใช้พฤติกรรมที่ไม่ได้กำหนด ดังนั้นกรณีนี้จะต้องเป็นไปไม่ได้และไม่จำเป็นต้องพิมพ์หมายเลข ifคำสั่งทั้งหมดสามารถปรับให้เหมาะสมได้ การให้เหตุผลย้อนหลังแบบนี้ได้รับอนุญาตตามมาตรฐานหรือไม่?


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

4
ผมคิดว่าเวลาเมื่อพฤติกรรมที่ไม่ได้กำหนดโผล่ออกมาจะไม่ได้กำหนด
eerorika

6
มาตรฐาน C ++ กล่าวอย่างชัดเจนว่าเส้นทางการดำเนินการที่มีพฤติกรรมที่ไม่ได้กำหนด ณ จุดใด ๆ นั้นไม่ได้กำหนดไว้อย่างสมบูรณ์ ฉันจะตีความว่ามันเป็นการบอกว่าโปรแกรมใด ๆ ที่มีพฤติกรรมที่ไม่ได้กำหนดบนเส้นทางนั้นไม่ได้กำหนดไว้อย่างสมบูรณ์ (ซึ่งรวมถึงผลลัพธ์ที่สมเหตุสมผลในส่วนอื่น ๆ แต่ไม่รับประกัน) คอมไพเลอร์มีอิสระที่จะใช้พฤติกรรมที่ไม่ได้กำหนดเพื่อแก้ไขโปรแกรมของคุณ blog.llvm.org/2011/05/what-every-c-programmer-should-know.htmlมีตัวอย่างดีๆ
Jens

4
@Jens: มันหมายถึงเส้นทางการดำเนินการจริงๆ อื่น ๆ const int i = 0; if (i) 5/i;ที่คุณได้รับเป็นปัญหามากกว่า
MSalters

1
คอมไพเลอร์โดยทั่วไปไม่สามารถพิสูจน์PrintToConsoleได้ว่าไม่ได้เรียกstd::exitดังนั้นจึงต้องทำการโทร
MSalters

คำตอบ:


66

การมีอยู่ของคำสั่งดังกล่าวในโปรแกรมที่กำหนดหมายความว่าโปรแกรมทั้งหมดไม่ได้กำหนดไว้หรือพฤติกรรมนั้นจะไม่ได้กำหนดก็ต่อเมื่อโฟลว์การควบคุมเข้าสู่คำสั่งนี้?

ทั้งสองอย่าง เงื่อนไขแรกแข็งแรงเกินไปและข้อที่สองอ่อนแอเกินไป

บางครั้งการเข้าถึงวัตถุจะเรียงตามลำดับ แต่มาตรฐานจะอธิบายพฤติกรรมของโปรแกรมนอกเวลา Danvil ยกมาแล้ว:

หากการดำเนินการใด ๆ ดังกล่าวมีการดำเนินการที่ไม่ได้กำหนดมาตรฐานสากลฉบับนี้ไม่ได้กำหนดข้อกำหนดเกี่ยวกับการดำเนินการตามโปรแกรมนั้นด้วยข้อมูลเข้านั้น (ไม่เกี่ยวกับการดำเนินการก่อนการดำเนินการที่ไม่ได้กำหนดไว้เป็นครั้งแรก)

สิ่งนี้สามารถตีความได้:

หากการดำเนินการของโปรแกรมทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดแสดงว่าทั้งโปรแกรมมีพฤติกรรมที่ไม่ได้กำหนดไว้

ดังนั้นคำสั่งที่ไม่สามารถเข้าถึงได้กับ UB จึงไม่ทำให้โปรแกรม UB คำสั่งที่เข้าถึงได้ว่า (เนื่องจากค่าของอินพุต) ไม่ถึงไม่ให้โปรแกรม UB นั่นเป็นเหตุผลที่เงื่อนไขแรกของคุณแข็งแรงเกินไป

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

ตัวอย่างที่สำคัญคือเมื่อเครื่องมือเพิ่มประสิทธิภาพใช้นามแฝงที่เข้มงวด จุดรวมของกฎการกำหนดนามแฝงที่เข้มงวดคือการอนุญาตให้คอมไพเลอร์สั่งการดำเนินการใหม่ที่ไม่สามารถเรียงลำดับใหม่ได้อย่างถูกต้องหากเป็นไปได้ว่าพอยน์เตอร์ที่เป็นปัญหาแทนหน่วยความจำเดียวกัน ดังนั้นหากคุณใช้ตัวชี้นามแฝงอย่างผิดกฎหมายและ UB เกิดขึ้นก็อาจส่งผลต่อคำสั่ง "before" ของคำสั่ง UB ได้อย่างง่ายดาย เท่าที่เครื่องจักรนามธรรมเกี่ยวข้องคำสั่ง UB ยังไม่ถูกดำเนินการ เท่าที่เกี่ยวข้องกับรหัสออบเจ็กต์จริงรหัสนั้นได้ถูกดำเนินการบางส่วนหรือทั้งหมดแล้ว แต่มาตรฐานไม่ได้พยายามที่จะลงรายละเอียดเกี่ยวกับความหมายของเครื่องมือเพิ่มประสิทธิภาพในการจัดลำดับคำสั่งใหม่หรือผลกระทบของสิ่งนั้นสำหรับ UB เพียงแค่ให้ใบอนุญาตการใช้งานผิดพลาดทันทีที่พอใจ

คุณสามารถคิดว่านี่คือ "UB มีไทม์แมชชีน"

เพื่อตอบตัวอย่างของคุณโดยเฉพาะ:

  • พฤติกรรมจะไม่ได้กำหนดก็ต่อเมื่ออ่าน 3
  • คอมไพเลอร์สามารถและกำจัดโค้ดได้อย่างตายตัวหากบล็อกพื้นฐานมีการดำเนินการบางอย่างที่ไม่ได้กำหนดไว้ พวกเขาได้รับอนุญาต (และฉันคาดเดาว่าจะทำ) ในกรณีที่ไม่ใช่บล็อกพื้นฐาน แต่ทุกสาขานำไปสู่ ​​UB ตัวอย่างนี้ไม่ใช่ตัวเลือกเว้นแต่PrintToConsole(3)จะทราบแน่ชัดว่าจะกลับมา มันอาจทำให้เกิดข้อยกเว้นหรืออะไรก็ได้

ตัวอย่างที่คล้ายกับที่สองของคุณคือตัวเลือก gcc -fdelete-null-pointer-checksซึ่งสามารถใช้รหัสเช่นนี้ได้ (ฉันยังไม่ได้ตรวจสอบตัวอย่างนี้โดยพิจารณาว่าเป็นตัวอย่างของแนวคิดทั่วไป):

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

และเปลี่ยนเป็น:

*p = 3;
std::cout << "3\n";

ทำไม? เพราะถ้าpเป็นโมฆะโค้ดก็มี UB อยู่ดีดังนั้นคอมไพเลอร์อาจถือว่าไม่เป็นโมฆะและปรับให้เหมาะสม เคอร์เนลลินุกซ์สะดุดเหนือสิ่งนี้ ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) เป็นหลักเนื่องจากทำงานในโหมดที่ไม่ควรอ้างถึงตัวชี้ค่าว่างเป็น UB คาดว่าจะส่งผลให้เกิดข้อยกเว้นฮาร์ดแวร์ที่กำหนดไว้ซึ่งเคอร์เนลสามารถจัดการได้ เมื่อเปิดใช้งานการเพิ่มประสิทธิภาพ gcc จำเป็นต้องใช้-fno-delete-null-pointer-checksเพื่อให้การรับประกันที่เกินมาตรฐานนั้น

ป.ล. คำตอบที่ใช้ได้จริงสำหรับคำถาม "พฤติกรรมที่ไม่ได้กำหนดจะเกิดขึ้นเมื่อใด" คือ "10 นาทีก่อนที่คุณจะออกเดินทางในวันนั้น"


4
อันที่จริงมีปัญหาด้านความปลอดภัยค่อนข้างน้อยเนื่องจากในอดีต โดยเฉพาะอย่างยิ่งการตรวจสอบที่เกิดขึ้นภายหลังการตรวจสอบมากเกินไปจะเสี่ยงต่อการถูกปรับให้เหมาะสมที่สุดเนื่องจากเหตุนี้ ยกตัวอย่างเช่น void can_add(int x) { if (x + 100 < x) complain(); }สามารถเพิ่มประสิทธิภาพออกไปอย่างสิ้นเชิงเพราะถ้าx+100 doesn'อะไรที่ล้นที่เกิดขึ้นและถ้าx+100 ไม่ล้นที่ UB ตามมาตรฐานดังนั้นไม่มีอะไรอาจจะเกิดขึ้น
fgp

3
@fgp: ใช่แล้วนั่นคือการเพิ่มประสิทธิภาพที่ผู้คนบ่นว่าขมขื่นหากพวกเขาเดินข้ามมันไปเพราะมันเริ่มรู้สึกว่าคอมไพเลอร์จงใจทำลายโค้ดของคุณเพื่อลงโทษคุณ "ทำไมฉันต้องเขียนแบบนั้นถ้าฉันต้องการให้คุณลบออก!" ;-) แต่ฉันคิดว่าบางครั้งมันก็มีประโยชน์สำหรับเครื่องมือเพิ่มประสิทธิภาพเมื่อจัดการกับนิพจน์ทางคณิตศาสตร์ที่ใหญ่ขึ้นโดยถือว่าไม่มีการล้นและหลีกเลี่ยงสิ่งที่มีราคาแพงซึ่งจำเป็นในกรณีเหล่านั้นเท่านั้น
Steve Jessop

2
จะถูกต้องหรือไม่ที่จะบอกว่าโปรแกรมไม่ได้กำหนดไว้หากผู้ใช้ไม่เคยเข้าสู่ 3 แต่ถ้าเขาป้อน 3 ระหว่างการดำเนินการการดำเนินการทั้งหมดจะไม่ได้กำหนดไว้ ทันทีที่มั่นใจ 100% ว่าโปรแกรมจะเรียกใช้พฤติกรรมที่ไม่ได้กำหนด (และไม่ช้าไปกว่านั้น) พฤติกรรมจะได้รับอนุญาตให้เป็นอะไรก็ได้ ข้อความเหล่านี้ของฉันถูกต้อง 100% หรือไม่?
usr

3
@usr: ฉันเชื่อว่าถูกต้องใช่ ด้วยตัวอย่างเฉพาะของคุณ (และตั้งสมมติฐานบางอย่างเกี่ยวกับความหลีกเลี่ยงไม่ได้ของข้อมูลที่กำลังประมวลผล) ฉันคิดว่าโดยหลักการแล้วการนำไปใช้งานสามารถมองไปข้างหน้าใน STDIN ที่บัฟเฟอร์ได้3หากต้องการและเตรียมออกจากบ้านในวันนั้นทันทีที่เห็น ขาเข้า
Steve Jessop

3
+1 พิเศษ (ถ้าทำได้) สำหรับ PS ของคุณ
Fred Larson

10

มาตรฐานระบุไว้ที่ 1.9 / 4

[หมายเหตุ: มาตรฐานสากลฉบับนี้ไม่ได้กำหนดข้อกำหนดเกี่ยวกับพฤติกรรมของโปรแกรมที่มีพฤติกรรมที่ไม่ได้กำหนดไว้ - หมายเหตุ]

ประเด็นที่น่าสนใจน่าจะเป็นความหมาย "ประกอบด้วย" หลังจากนั้นเล็กน้อยที่ 1.9 / 5 จะระบุ:

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

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

อย่างไรก็ตามปัญหาที่แตกต่างกันคือสมมติฐานที่ขึ้นอยู่กับพฤติกรรมที่ไม่ได้กำหนดไว้ในระหว่างการสร้างโค้ด ดูคำตอบของ Steve Jessop สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับเรื่องนี้


1
ถ้าเอาตามตัวอักษรนั่นคือโทษประหารสำหรับโปรแกรมจริงทั้งหมดที่มีอยู่
usr

6
ฉันไม่คิดว่าคำถามคือถ้า UB อาจปรากฏขึ้นก่อนที่รหัสจะถึงจริง คำถามตามที่ฉันเข้าใจก็คือหาก UB อาจปรากฏขึ้นหากไม่สามารถเข้าถึงรหัสได้ และแน่นอนคำตอบคือ "ไม่"
sepp2k

มาตรฐานยังไม่ชัดเจนเกี่ยวกับเรื่องนี้ใน 1.9 / 4 แต่ 1.9 / 5 สามารถตีความได้ว่าคุณพูดอะไร
Danvil

1
หมายเหตุไม่ใช่กฎเกณฑ์ 1.9 / 5 เหนือกว่าโน้ตใน 1.9 / 4
MSalters

5

ตัวอย่างคำแนะนำคือ

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

ทั้ง GCC ปัจจุบันและ Clang ปัจจุบันจะปรับให้เหมาะสม (บน x86) เป็น

xorl %eax,%eax
ret

เนื่องจากอนุมานว่าxเป็นศูนย์เสมอจาก UB ในif (x)เส้นทางควบคุม GCC จะไม่แจ้งเตือนการใช้ค่าที่ไม่ได้เริ่มต้นด้วยซ้ำ! (เนื่องจากบัตรผ่านที่ใช้ตรรกะข้างต้นทำงานก่อนบัตรผ่านที่สร้างคำเตือนค่าที่ไม่ได้เริ่มต้น)


1
ตัวอย่างที่น่าสนใจ ค่อนข้างน่ารังเกียจที่การเปิดใช้งานการเพิ่มประสิทธิภาพจะซ่อนคำเตือนไว้ สิ่งนี้ไม่ได้จัดทำเป็นเอกสาร - เอกสาร GCC บอกเพียงว่าการเปิดใช้งานการเพิ่มประสิทธิภาพจะทำให้เกิดคำเตือนมากขึ้น
sleske

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

@zwol: ฉันสงสัยว่า "การเพิ่มประสิทธิภาพ" ที่เป็นผลมาจากการกำจัดรหัสตายดังกล่าวทำให้โค้ดที่มีประโยชน์มีขนาดเล็กลงได้อย่างไรและจะทำให้โปรแกรมเมอร์สร้างโค้ดให้ใหญ่ขึ้นได้มากเพียงใด (เช่นการเพิ่มโค้ดเพื่อเริ่มต้นaแม้ว่าในทุกสถานการณ์ uninitialized aจะถูกส่งผ่านไปยังฟังก์ชันที่ฟังก์ชันจะไม่ทำอะไรเลย)?
supercat

@supercat ฉันไม่ได้ยุ่งเกี่ยวกับงานคอมไพเลอร์มากว่า 10 ปีแล้วและแทบจะเป็นไปไม่ได้เลยที่จะให้เหตุผลเกี่ยวกับการเพิ่มประสิทธิภาพจากตัวอย่างของเล่น การเพิ่มประสิทธิภาพประเภทนี้มีแนวโน้มที่จะเกี่ยวข้องกับการลดขนาดโค้ดโดยรวม 2-5% ในแอปพลิเคชันจริงถ้าฉันจำไม่ผิด
zwol

1
@supercat 2-5% นั้นใหญ่มากเมื่อสิ่งเหล่านี้ดำเนินไป ฉันเคยเห็นคนเหงื่อ 0.1%
zwol

4

ร่างการทำงาน C ++ ปัจจุบันกล่าวใน 1.9.4 ว่า

มาตรฐานสากลฉบับนี้ไม่ได้กำหนดข้อกำหนดเกี่ยวกับพฤติกรรมของโปรแกรมที่มีพฤติกรรมที่ไม่ถูกต้อง

จากสิ่งนี้ฉันจะบอกว่าโปรแกรมที่มีพฤติกรรมที่ไม่ได้กำหนดบนเส้นทางการดำเนินการใด ๆ สามารถทำอะไรก็ได้ทุกครั้งที่ดำเนินการ

มีบทความที่ดีจริงๆสองเรื่องเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดและสิ่งที่คอมไพเลอร์มักจะทำ:


1
นั่นไม่สมเหตุสมผล ฟังก์ชันนี้int f(int x) { if (x > 0) return 100/x; else return 100; }จะไม่เรียกใช้พฤติกรรมที่ไม่ได้กำหนดอย่างแน่นอนแม้ว่า100/0จะไม่ได้กำหนดไว้ก็ตาม
fgp

1
@fgp อะไรมาตรฐาน (โดยเฉพาะ 1.9 / 5) กล่าวว่าแม้ว่าคือว่าถ้าไม่ได้กำหนดพฤติกรรมสามารถไปถึงได้ก็ไม่ได้ว่าเมื่อมันมาถึง ตัวอย่างเช่นprintf("Hello, World"); *((char*)NULL) = 0 ไม่รับประกันว่าจะพิมพ์อะไร สิ่งนี้ช่วยในการปรับให้เหมาะสมเนื่องจากคอมไพลเลอร์สามารถสั่งการดำเนินการใหม่ได้อย่างอิสระ (แน่นอนว่าจะมีข้อ จำกัด ในการพึ่งพา) ซึ่งจะรู้ว่าจะเกิดขึ้นในที่สุดโดยไม่ต้องคำนึงถึงพฤติกรรมที่ไม่ได้กำหนดไว้
fgp

ฉันจะบอกว่าโปรแกรมที่มีฟังก์ชันของคุณไม่มีพฤติกรรมที่ไม่ได้กำหนดไว้เนื่องจากไม่มีข้อมูลที่จะประเมิน 100/0
Jens

1
ถูกต้อง - ดังนั้นสิ่งที่สำคัญคือ UB สามารถถูกกระตุ้นได้จริงหรือไม่ไม่ว่าจะสามารถถูกกระตุ้นในทางทฤษฎีได้หรือไม่ หรือคุณพร้อมที่จะโต้แย้งที่int x,y; std::cin >> x >> y; std::cout << (x+y);ได้รับอนุญาตให้พูดว่า "1 + 1 = 17" เพียงเพราะมีอินพุตบางส่วนที่x+yล้น (ซึ่งก็คือ UB เนื่องจากintเป็นประเภทที่มีการลงนาม)
fgp

อย่างเป็นทางการฉันจะบอกว่าโปรแกรมมีพฤติกรรมที่ไม่ได้กำหนดเนื่องจากมีอินพุตที่เรียกใช้ แต่คุณคิดถูกที่สิ่งนี้ไม่สมเหตุสมผลในบริบทของ C ++ เพราะจะเป็นไปไม่ได้ที่จะเขียนโปรแกรมโดยไม่มีพฤติกรรมที่ไม่ได้กำหนด ฉันต้องการเมื่อมีพฤติกรรมที่ไม่ได้กำหนดใน C ++ น้อยลง แต่นั่นไม่ใช่วิธีการทำงานของภาษา (และมีเหตุผลที่ดีสำหรับสิ่งนี้ แต่พวกเขาไม่เกี่ยวข้องกับการใช้งานประจำวันของฉัน ... )
Jens

3

คำว่าพฤติกรรมหมายถึงสิ่งที่กำลังทำทำStatemenr ที่ไม่ถูกประหารไม่ใช่ "พฤติกรรม"

ภาพประกอบ:

*ptr = 0;

เป็นพฤติกรรมที่ไม่ได้กำหนดหรือไม่? สมมติว่าเรามั่นใจ 100%ptr == nullptrน้อยหนึ่งครั้งในระหว่างการเรียกใช้โปรแกรม คำตอบควรเป็นใช่

อะไรประมาณนี้

 if (ptr) *ptr = 0;

ไม่ได้กำหนดไว้หรือไม่? (จำptr == nullptrอย่างน้อยหนึ่งครั้งได้หรือไม่) ฉันหวังว่าจะไม่เป็นอย่างนั้นคุณจะไม่สามารถเขียนโปรแกรมที่มีประโยชน์ได้เลย

ไม่ได้รับอันตรายใด ๆ ในการทำคำตอบนี้


3

พฤติกรรมที่ไม่ได้กำหนดจะเกิดขึ้นเมื่อโปรแกรมจะทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดไม่ว่าจะเกิดอะไรขึ้นต่อไป อย่างไรก็ตามคุณให้ตัวอย่างต่อไปนี้

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

เว้นแต่คอมไพลเลอร์จะทราบคำจำกัดความของPrintToConsoleก็ไม่สามารถลบif (num == 3)เงื่อนไขได้ สมมติว่าคุณมีส่วนหัวของระบบที่มีการประกาศดังต่อไปนี้LongAndCamelCaseStdio.hPrintToConsole

void PrintToConsole(int);

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

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

จริงๆแล้วคอมไพเลอร์ต้องสมมติว่าฟังก์ชันใด ๆ โดยพลการที่คอมไพเลอร์ไม่รู้ว่ามันทำอะไรอยู่อาจออกหรือทำให้เกิดข้อยกเว้น (ในกรณีของ C ++) คุณสามารถสังเกตได้ว่า*((char*)NULL) = 0;จะไม่ถูกดำเนินการเนื่องจากการดำเนินการจะไม่ดำเนินต่อไปหลังจากนั้นPrintToConsoleโทร

พฤติกรรมที่ไม่ได้กำหนดจะเกิดขึ้นเมื่อใด PrintToConsoleส่งคืนจริง คอมไพเลอร์คาดว่าสิ่งนี้จะไม่เกิดขึ้น (เนื่องจากจะทำให้โปรแกรมดำเนินการตามพฤติกรรมที่ไม่ได้กำหนดไม่ว่าจะเกิดอะไรขึ้นก็ตาม) ดังนั้นอะไรก็เกิดขึ้นได้

อย่างไรก็ตามลองพิจารณาอย่างอื่น สมมติว่าเรากำลังทำการตรวจสอบค่าว่างและใช้ตัวแปรหลังจากการตรวจสอบค่าว่าง

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

ในกรณีนี้สังเกตได้ง่ายว่าlol_null_checkต้องใช้ตัวชี้ที่ไม่ใช่ NULL การกำหนดให้กับwarningตัวแปรGlobal non-volatile ไม่ใช่สิ่งที่สามารถออกจากโปรแกรมหรือทำให้เกิดข้อยกเว้นใด ๆ pointerนี้ยังไม่ระเหยจึงไม่สามารถได้อย่างน่าอัศจรรย์เปลี่ยนค่าในช่วงกลางของฟังก์ชั่น (ถ้ามันไม่เป็นพฤติกรรมที่ไม่ได้กำหนด) การโทรlol_null_check(NULL)จะทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดซึ่งอาจทำให้ไม่สามารถกำหนดตัวแปรได้ (เนื่องจาก ณ จุดนี้จะทราบข้อเท็จจริงที่ว่าโปรแกรมเรียกใช้พฤติกรรมที่ไม่ได้กำหนดไว้)

อย่างไรก็ตามพฤติกรรมที่ไม่ได้กำหนดหมายความว่าโปรแกรมสามารถทำอะไรก็ได้ ดังนั้นจึงไม่มีสิ่งใดหยุดพฤติกรรมที่ไม่ได้กำหนดไว้ไม่ให้ย้อนเวลากลับไปและทำให้โปรแกรมของคุณหยุดทำงานก่อนที่จะint main()ดำเนินการบรรทัดแรก เป็นพฤติกรรมที่ไม่ได้กำหนดไม่จำเป็นต้องมีเหตุผล มันอาจผิดพลาดหลังจากพิมพ์ 3 แต่พฤติกรรมที่ไม่ได้กำหนดจะย้อนเวลากลับไปและหยุดทำงานก่อนที่คุณจะพิมพ์ 3 และใครจะรู้ว่าพฤติกรรมที่ไม่ได้กำหนดอาจเขียนทับ RAM ระบบของคุณและทำให้ระบบของคุณพังใน 2 สัปดาห์ต่อมา ในขณะที่โปรแกรมที่ไม่ได้กำหนดของคุณไม่ทำงาน


คะแนนที่ถูกต้องทั้งหมด PrintToConsoleเป็นความพยายามของฉันในการแทรกผลข้างเคียงของโปรแกรมภายนอกที่มองเห็นได้แม้จะเกิดปัญหาและมีการจัดลำดับอย่างมาก ฉันต้องการสร้างสถานการณ์ที่เราสามารถบอกได้อย่างแน่นอนว่าข้อความนี้ได้รับการปรับให้เหมาะสมหรือไม่ แต่คุณคิดถูกแล้วที่มันอาจจะไม่กลับมาอีก; ตัวอย่างการเขียนถึง global อาจมีการเพิ่มประสิทธิภาพอื่น ๆ ที่ไม่เกี่ยวข้องกับ UB ตัวอย่างเช่น global ที่ไม่ได้ใช้สามารถลบได้ คุณมีแนวคิดในการสร้างผลข้างเคียงภายนอกด้วยวิธีที่รับประกันว่าจะคืนการควบคุมหรือไม่?
usr

ผลข้างเคียงที่สังเกตได้จากภายนอกโลกสามารถสร้างขึ้นโดยใช้รหัสที่คอมไพเลอร์สามารถรับคืนได้หรือไม่? ตามความเข้าใจของฉันแม้แต่วิธีการที่อ่านvolatileตัวแปรเพียงอย่างเดียวก็สามารถทริกเกอร์การดำเนินการ I / O ได้อย่างถูกต้องซึ่งสามารถขัดจังหวะเธรดปัจจุบันได้ทันที จากนั้นตัวจัดการขัดจังหวะสามารถฆ่าเธรดก่อนที่จะมีโอกาสดำเนินการอย่างอื่น ฉันไม่เห็นเหตุผลที่คอมไพเลอร์สามารถผลักดันพฤติกรรมที่ไม่ได้กำหนดก่อนถึงจุดนั้น
supercat

จากมุมมองของมาตรฐาน C จะไม่มีอะไรผิดกฎหมายเกี่ยวกับการมีพฤติกรรมที่ไม่ได้กำหนดทำให้คอมพิวเตอร์ส่งข้อความไปยังบางคนที่จะติดตามและทำลายหลักฐานทั้งหมดของการกระทำก่อนหน้านี้ของโปรแกรม แต่หากการดำเนินการสามารถยุติเธรดได้ ทุกสิ่งที่เรียงตามลำดับก่อนการกระทำนั้นจะต้องเกิดขึ้นก่อนพฤติกรรมที่ไม่ได้กำหนดใด ๆ ที่เกิดขึ้นหลังจากนั้น
supercat

1

หากโปรแกรมถึงคำสั่งที่เรียกใช้พฤติกรรมที่ไม่ได้กำหนดจะไม่มีข้อกำหนดใด ๆ วางอยู่บนผลลัพธ์ / พฤติกรรมใด ๆ ของโปรแกรมใด ๆ ไม่สำคัญว่าพฤติกรรมที่ไม่ได้กำหนดจะเกิดขึ้น "ก่อน" หรือ "หลัง"

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


1
ด้วยความอยากรู้อยากเห็นเมื่อไหร่ที่__builtin_unreachable()เริ่มมีผลกระทบทั้งย้อนหลังและไปข้างหน้าในเวลา? ให้บางอย่างเช่นextern volatile uint32_t RESET_TRIGGER; void RESET(void) { RESET_TRIGGER = 0xAA55; __memorybarrier(); __builtin_unreachable(); }ฉันเห็นbuiltin_unreachable()ว่าเป็นสิ่งที่ดีที่จะให้คอมไพเลอร์รู้ว่าสามารถละเว้นreturnคำสั่งได้ แต่จะค่อนข้างแตกต่างจากการบอกว่าสามารถละรหัสก่อนหน้าได้
supercat

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

@usr: ฉันคิดว่าคอมไพเลอร์ระดับต่ำควรถือว่าการเข้าถึงแบบระเหยเป็นการเรียกเมธอดทึบแสง แต่ทั้งเสียงดังหรือ gcc ไม่ทำเช่นนั้น เหนือสิ่งอื่นใดการเรียกเมธอดทึบแสงอาจทำให้ไบต์ทั้งหมดของวัตถุใด ๆ ที่แอดเดรสถูกเปิดเผยกับโลกภายนอกและrestrictตัวชี้แบบสดที่ไม่มีการเข้าถึงหรือไม่สามารถเข้าถึงได้จะถูกเขียนโดยใช้unsigned char*.
supercat

@usr: หากคอมไพเลอร์ไม่ถือว่าการเข้าถึงแบบระเหยเป็นวิธีการเรียกแบบทึบแสงเกี่ยวกับการเข้าถึงวัตถุที่เปิดเผยฉันไม่เห็นเหตุผลใดเป็นพิเศษที่จะคาดหวังว่ามันจะทำเช่นนั้นเพื่อวัตถุประสงค์อื่น มาตรฐานไม่ต้องการให้มีการใช้งานเนื่องจากมีแพลตฟอร์มฮาร์ดแวร์บางตัวที่คอมไพเลอร์อาจทราบผลกระทบที่เป็นไปได้ทั้งหมดจากการเข้าถึงที่ไม่แน่นอน อย่างไรก็ตามคอมไพเลอร์ที่เหมาะสำหรับการใช้งานแบบฝังควรตระหนักว่าการเข้าถึงแบบระเหยอาจทริกเกอร์ฮาร์ดแวร์ที่ไม่ได้ถูกคิดค้นขึ้นเมื่อเขียนคอมไพเลอร์
supercat

@supercat ฉันคิดว่าคุณพูดถูก ดูเหมือนว่าการดำเนินการที่ผันผวนจะ "ไม่มีผลต่อเครื่องนามธรรม" ดังนั้นจึงไม่สามารถยุติโปรแกรมหรือทำให้เกิดผลข้างเคียงได้
usr

1

มาตรฐานจำนวนมากสำหรับสิ่งต่างๆหลายประเภทใช้ความพยายามอย่างมากในการอธิบายสิ่งต่างๆที่การนำไปใช้งานไม่ควรทำหรือไม่ควรทำโดยใช้ระบบการตั้งชื่อที่คล้ายกับที่กำหนดไว้ในIETF RFC 2119 (แม้ว่าจะไม่จำเป็นต้องอ้างถึงคำจำกัดความในเอกสารนั้นก็ตาม) ในหลาย ๆ กรณีคำอธิบายของสิ่งที่การนำไปใช้งานควรทำยกเว้นในกรณีที่สิ่งเหล่านั้นจะไร้ประโยชน์หรือไม่สามารถใช้งานได้จริงมีความสำคัญมากกว่าข้อกำหนดที่การใช้งานที่สอดคล้องกันทั้งหมดจะต้องเป็นไปตาม

น่าเสียดายที่มาตรฐาน C และ C ++ มีแนวโน้มที่จะละเว้นคำอธิบายของสิ่งต่าง ๆ ซึ่งแม้ว่าจะไม่จำเป็นต้องใช้ 100% แต่ก็ควรได้รับการคาดหวังจากการใช้งานที่มีคุณภาพซึ่งไม่ได้บันทึกพฤติกรรมที่ขัดกัน ข้อเสนอแนะที่ว่าการใช้งานควรทำบางสิ่งบางอย่างอาจถูกมองว่าเป็นนัยว่าสิ่งที่ไม่ด้อยกว่าและในกรณีที่โดยทั่วไปแล้วจะเห็นได้ชัดว่าพฤติกรรมใดจะเป็นประโยชน์หรือปฏิบัติได้จริงเมื่อเทียบกับการนำไปใช้ไม่ได้และไม่มีประโยชน์ในการนำไปใช้งานนั้นจะมี ความต้องการเพียงเล็กน้อยที่มาตรฐานจะแทรกแซงการตัดสินดังกล่าว

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


เป็นจุดที่ดีที่การหลีกเลี่ยง UB อาจทำให้เสียค่าใช้จ่ายสำหรับรหัสผู้ใช้
usr

@usr: มันเป็นจุดที่คนสมัยใหม่คิดถึงอย่างสิ้นเชิง ฉันควรเพิ่มตัวอย่างหรือไม่? เช่นหากโค้ดต้องประเมินว่าx*y < zเมื่อx*yไหร่ไม่ล้นและในกรณีที่โอเวอร์โฟลว์ให้ผลเป็น 0 หรือ 1 ตามอำเภอใจ แต่ไม่มีผลข้างเคียงก็ไม่มีเหตุผลในแพลตฟอร์มส่วนใหญ่ว่าทำไมการตอบสนองความต้องการที่สองและสามควรมีราคาแพงกว่า การพบกันครั้งแรก แต่วิธีใดก็ตามในการเขียนนิพจน์เพื่อรับประกันพฤติกรรมที่กำหนดมาตรฐานในทุกกรณีในบางกรณีจะเพิ่มต้นทุนที่สำคัญ การเขียนนิพจน์(int64_t)x*y < zสามารถเพิ่มต้นทุนการคำนวณได้มากกว่าสี่เท่า ...
supercat

... บนแพลตฟอร์มบางส่วนและเขียนมันเป็น(int)((unsigned)x*y) < zจะป้องกันไม่ให้คอมไพเลอร์จากการจ้างงานในสิ่งที่อาจจะได้รับการแทนพีชคณิตประโยชน์ (เช่นถ้ามันรู้ว่าxและzมีความเท่าเทียมกันและบวกมันจะลดความซับซ้อนของการแสดงออกเดิมy<0แต่รุ่นที่ใช้ไม่ได้ลงนาม จะบังคับให้คอมไพเลอร์ทำการคูณ) หากคอมไพลเลอร์สามารถรับประกันได้แม้ว่า Standard จะไม่ได้บังคับก็ตาม แต่ก็ยังคงรักษาข้อกำหนด "yield 0 หรือ 1 โดยไม่มีผลข้างเคียง" โค้ดผู้ใช้อาจให้โอกาสในการปรับแต่งคอมไพลเลอร์ที่ไม่สามารถรับได้
supercat

ใช่ดูเหมือนว่าพฤติกรรมที่ไม่ได้กำหนดรูปแบบที่อ่อนโยนกว่านี้จะเป็นประโยชน์ที่นี่ โปรแกรมเมอร์สามารถเปิดโหมดที่ทำให้เกิดx*yการปล่อยค่าปกติในกรณีที่โอเวอร์โฟล แต่มีค่าใด ๆ เลย UB ที่กำหนดค่าได้ใน C / C ++ ดูเหมือนจะสำคัญสำหรับฉัน
usr

@usr: หากผู้เขียน C89 Standard มีความจริงในการกล่าวว่าการส่งเสริมค่านิยมสั้น ๆ ที่ไม่ได้ลงนามในการลงชื่อเข้าใช้เป็นการเปลี่ยนแปลงที่ร้ายแรงที่สุดและไม่ใช่คนโง่ที่เพิกเฉยนั่นอาจหมายความว่าพวกเขาคาดหวังว่าในกรณีที่แพลตฟอร์มมี ในการกำหนดการรับประกันพฤติกรรมที่เป็นประโยชน์การนำไปใช้งานสำหรับแพลตฟอร์มดังกล่าวได้ทำให้การค้ำประกันเหล่านั้นพร้อมใช้งานสำหรับโปรแกรมเมอร์และโปรแกรมเมอร์ได้ใช้ประโยชน์จากสิ่งเหล่านี้คอมไพเลอร์สำหรับแพลตฟอร์มดังกล่าวจะยังคงเสนอการรับประกันพฤติกรรมดังกล่าวต่อไปไม่ว่ามาตรฐานจะสั่งให้หรือไม่ก็ตาม
supercat
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.