ทำไม f (i = -1, i = -1) พฤติกรรมที่ไม่ได้กำหนด?


267

ฉันกำลังอ่านเกี่ยวกับลำดับของการละเมิดการประเมินผลและพวกเขายกตัวอย่างที่ทำให้ฉันสงสัย

1) หากผลข้างเคียงของวัตถุสเกลาร์นั้นไม่ได้ถูกจัดลำดับสัมพันธ์กับผลข้างเคียงอื่นในวัตถุสเกลาร์เดียวกันพฤติกรรมนั้นจะไม่ได้กำหนดไว้

// snip
f(i = -1, i = -1); // undefined behavior

ในบริบทiนี้เป็นวัตถุสเกลาร์ซึ่งเห็นได้ชัดว่าหมายถึง

ประเภทเลขคณิต (3.9.1), ประเภทการแจงนับ, ประเภทตัวชี้, ตัวชี้ไปยังประเภทสมาชิก (3.9.2), std :: nullptr_t, และเวอร์ชันที่ผ่านการรับรอง cv ของประเภทเหล่านี้ (3.9.3) เรียกว่าประเภทสเกลาร์

ฉันไม่เห็นว่าคำสั่งนั้นคลุมเครือในกรณีนั้นอย่างไร สำหรับฉันดูเหมือนว่าไม่ว่าอาร์กิวเมนต์แรกหรืออาร์กิวเมนต์ที่สองจะได้รับการประเมินก่อนiจะสิ้นสุดลง-1และอาร์กิวเมนต์ทั้งสองก็เช่น-1กัน

บางคนช่วยอธิบายได้ไหม?


UPDATE

ฉันขอขอบคุณการอภิปรายทั้งหมด ถึงตอนนี้ฉันชอบคำตอบของ @ harmicมากเพราะมันทำให้เกิดข้อผิดพลาดและความซับซ้อนของการกำหนดคำสั่งนี้แม้จะมองไปข้างหน้าอย่างรวดเร็วในทันที @ acheong87ชี้ให้เห็นปัญหาบางอย่างที่เกิดขึ้นเมื่อใช้การอ้างอิง แต่ฉันคิดว่ามันเป็นมุมฉากกับผลข้างเคียงที่ตามมาของคำถามนี้


สรุป

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

สำหรับสาเหตุอะไร

คำตอบหลักมาจากPaul DraperโดยMartin Jให้คำตอบที่คล้ายกัน แต่ไม่ครอบคลุม คำตอบของ Paul Draper ทำให้เดือดร้อน

มันเป็นพฤติกรรมที่ไม่ได้กำหนดเพราะมันไม่ได้กำหนดว่าพฤติกรรมคืออะไร

คำตอบนั้นโดยรวมนั้นดีมากในแง่ของการอธิบายสิ่งที่มาตรฐาน C ++ พูด นอกจากนี้ยังอยู่บางกรณีที่เกี่ยวข้องกับการ UB เช่นและf(++i, ++i); f(i=1, i=-1);ในกรณีแรกที่เกี่ยวข้องมันไม่ชัดเจนว่าการโต้แย้งครั้งแรกควรเป็นi+1และครั้งที่สองi+2หรือในทางกลับกัน; ในครั้งที่สองมันไม่ชัดเจนว่าiควรจะเป็น 1 หรือ -1 หลังจากการเรียกใช้ฟังก์ชัน ทั้งสองกรณีนี้เป็น UB เพราะอยู่ภายใต้กฎต่อไปนี้:

หากผลข้างเคียงของวัตถุสเกลาร์นั้นสัมพันธ์กันกับผลข้างเคียงอื่นในวัตถุสเกลาร์เดียวกันพฤติกรรมนั้นจะไม่ได้กำหนดไว้

ดังนั้นจึงf(i=-1, i=-1)เป็น UB เพราะมันตกอยู่ภายใต้กฎเดียวกันแม้ว่าความตั้งใจของโปรแกรมเมอร์คือ (IMHO) ชัดเจนและไม่คลุมเครือ

พอลเดรเปอร์ยังทำให้ชัดเจนในบทสรุปของเขาว่า

มันสามารถถูกกำหนดพฤติกรรม? ใช่. มันถูกกำหนดไว้? เลขที่

ซึ่งนำเรามาสู่คำถามที่ว่า "ด้วยเหตุผล / วัตถุประสงค์อะไรที่ถูกf(i=-1, i=-1)ทิ้งไว้เป็นพฤติกรรมที่ไม่ได้กำหนด"

ด้วยเหตุผลใด / วัตถุประสงค์

แม้ว่าจะมีการกำกับดูแลบางอย่าง (อาจจะประมาท) ในมาตรฐาน C ++ แต่การละเว้นส่วนใหญ่นั้นสมเหตุสมผลและให้บริการตามวัตถุประสงค์เฉพาะ แม้ว่าฉันจะทราบว่าจุดประสงค์มักจะ "ทำให้งานของนักเขียนคอมไพเลอร์ง่ายขึ้น" หรือ "รหัสเร็วขึ้น" แต่ฉันก็สนใจที่จะรู้ว่ามีเหตุผลอะไรที่ดี f(i=-1, i=-1) ในฐานะ UB

harmicและsupercatให้คำตอบหลักที่ให้เหตุผลสำหรับ UB Harmic ชี้ให้เห็นว่าคอมไพเลอร์ที่ปรับให้เหมาะสมซึ่งอาจแยกการปฏิบัติการปรมาณูออกมาอย่างเห็นได้ชัดเป็นคำสั่งหลายเครื่องและมันอาจจะแทรกคำแนะนำเหล่านั้นเพื่อความเร็วที่เหมาะสม สิ่งนี้อาจนำไปสู่ผลลัพธ์ที่น่าประหลาดใจ: iจบลงที่ -2 ในสถานการณ์ของเขา! ดังนั้นอันตรายแสดงให้เห็นว่าการกำหนดค่าเดียวกันให้กับตัวแปรมากกว่าหนึ่งครั้งสามารถมีผลร้ายหากการดำเนินการจะตามมา

supercat ให้คำอธิบายเกี่ยวกับข้อผิดพลาดในการพยายามf(i=-1, i=-1)ทำสิ่งที่ดูเหมือนว่าควรจะทำ เขาชี้ให้เห็นว่าในบางสถาปัตยกรรมมีข้อ จำกัด อย่างหนักต่อการเขียนหลายอย่างพร้อมกันไปยังที่อยู่หน่วยความจำเดียวกัน f(i=-1, i=-1)เรียบเรียงอาจมีช่วงเวลาที่ยากจับนี้ถ้าเราได้จัดการกับสิ่งที่น้อยกว่าเล็กน้อย

davidfยังให้ตัวอย่างของคำสั่งแบบแทรกสอดคล้ายกับของ harmic

ถึงแม้ว่าจะมีทั้งฮาร์โมนิกตัวอย่างของซูเปอร์แคทและดาวิฟก็ถูกประดิษฐ์ขึ้นมาบ้าง แต่พวกมันก็ยังคงให้เหตุผลที่จับf(i=-1, i=-1)ต้องได้

ฉันยอมรับคำตอบของ harmic เพราะมันทำงานได้ดีที่สุดในการจัดการกับความหมายทั้งหมดว่าทำไมถึงแม้ว่าคำตอบของ Paul Draper พูดถึงส่วน "สาเหตุที่ทำให้" ดีขึ้น

คำตอบอื่น ๆ

JohnBชี้ให้เห็นว่าหากเราพิจารณาตัวดำเนินการที่ได้รับมอบหมายมากเกินไป (แทนที่จะเป็นสเกลาร์ธรรมดา) เราก็สามารถประสบปัญหาเช่นกัน


1
วัตถุสเกลาร์เป็นวัตถุประเภทสเกลาร์ ดู 3.9 / 9: "ประเภทเลขคณิต (3.9.1) ประเภทนับประเภทตัวชี้ชี้ไปยังประเภทสมาชิก (3.9.2) std::nullptr_tและพันธุ์ที่ผ่านการคัดเลือกรุ่นของประเภทนี้ (3.9.3) จะเรียกว่าประเภทเกลา "
Rob Kennedy

1
อาจมีข้อผิดพลาดในหน้าและพวกเขาจริงf(i-1, i = -1)หรือสิ่งที่คล้ายกัน
นาย Lister

ดูคำถามนี้: stackoverflow.com/a/4177063/71074
Robert S. Barnes

@ RobKennedy ขอบคุณ "ประเภทคณิตศาสตร์" รวมบูลหรือไม่
Nicu Stiurca

1
SchighSchagh การปรับปรุงของคุณควรอยู่ในส่วนคำตอบ
Grijesh Chauhan

คำตอบ:


343

เนื่องจากการดำเนินการไม่ได้เกิดขึ้นตามมาจึงไม่มีอะไรจะบอกได้ว่าคำแนะนำในการปฏิบัติงานที่มอบหมายไม่สามารถสอดแทรกได้ อาจเป็นการดีที่สุดที่จะทำทั้งนี้ขึ้นอยู่กับสถาปัตยกรรมของ CPU หน้าอ้างอิงอ้างถึงสิ่งนี้:

หาก A ไม่ได้ถูกจัดลำดับก่อน B และ B จะไม่ถูกจัดลำดับก่อน A ดังนั้นจะมีความเป็นไปได้สองอย่าง:

  • การประเมินผลของ A และ B จะไม่เกิดขึ้นตามลำดับ: อาจดำเนินการในลำดับใดก็ได้และอาจทับซ้อน (ภายในเธรดเดี่ยวของการดำเนินการคอมไพเลอร์อาจสอดแทรกคำสั่ง CPU ที่ประกอบด้วย A และ B)

  • การประเมินของ A และ B นั้นไม่ได้กำหนดลำดับไว้: อาจดำเนินการในลำดับใดก็ได้ แต่อาจไม่ทับซ้อน: A จะเสร็จสมบูรณ์ก่อน B หรือ B จะเสร็จสมบูรณ์ก่อน A คำสั่งอาจตรงกันข้ามในครั้งถัดไปที่มีนิพจน์เดียวกัน ได้รับการประเมิน

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

ตัวอย่างเช่นลองนึกภาพว่ามันมีประสิทธิภาพมากกว่าที่จะเป็นศูนย์ความจำแล้วลดลงเมื่อเทียบกับการโหลดค่า -1 เข้าจากนั้นสิ่งนี้:

f(i=-1, i=-1)

อาจกลายเป็น:

clear i
clear i
decr i
decr i

ตอนนี้ฉันคือ -2

อาจเป็นตัวอย่างปลอม แต่เป็นไปได้


59
ตัวอย่างที่ดีมากของวิธีการที่นิพจน์สามารถทำสิ่งที่ไม่คาดคิดในขณะที่สอดคล้องกับกฎการเรียง ใช่มีการประดิษฐ์เล็กน้อย แต่เป็นรหัสที่ฉันถามในตอนแรก :)
Nicu Stiurca

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

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

1
+ 1e + 6 (ตกลง, +1) สำหรับจุดที่คอมไพล์โค้ดไม่ได้เป็นอย่างที่คุณคาดหวัง เครื่องมือเพิ่มประสิทธิภาพเป็นสิ่งที่ดีจริงๆในการขว้างโค้งมนไปที่คุณเมื่อคุณไม่ปฏิบัติตามกฎ: P
Corey

3
บนตัวประมวลผล Arm โหลด 32 บิตสามารถทำได้มากถึง 4 คำสั่ง: ทำได้load 8bit immediate and shiftสูงสุด 4 ครั้ง โดยทั่วไปแล้วคอมไพเลอร์จะทำการกำหนดที่อยู่ทางอ้อมเพื่อดึงตัวเลขในรูปแบบตารางเพื่อหลีกเลี่ยงปัญหานี้ (-1 สามารถทำได้ใน 1 คำสั่ง แต่สามารถเลือกอีกตัวอย่างได้)
ctrl-alt-delor

208

แรก "วัตถุเกลา" หมายถึงประเภทเช่นint, floatหรือตัวชี้ (ดูคืออะไรวัตถุเกลาใน C ++? )


ประการที่สองมันอาจดูเหมือนชัดเจนมากขึ้นว่า

f(++i, ++i);

จะมีพฤติกรรมที่ไม่ได้กำหนด แต่

f(i = -1, i = -1);

ไม่ชัดเจน

ตัวอย่างที่แตกต่างกันเล็กน้อย:

int i;
f(i = 1, i = -1);
std::cout << i << "\n";

สิ่งที่เกิดขึ้นได้รับมอบหมาย "สุดท้าย" i = 1หรือi = -1? มันไม่ได้กำหนดไว้ในมาตรฐาน จริงๆแล้วนั่นiอาจหมายถึง5(ดูคำตอบที่เป็นอันตรายสำหรับคำอธิบายที่เป็นไปได้อย่างสมบูรณ์สำหรับกรณีที่เกิดกรณีนี้ขึ้น) หรือคุณโปรแกรมสามารถ segfault หรือฟอร์แมตฮาร์ดดิสก์ของคุณใหม่

แต่ตอนนี้คุณถามว่า: "ตัวอย่างของฉันเป็นอย่างไรฉันใช้ค่าเดียวกัน ( -1) สำหรับการมอบหมายทั้งสองสิ่งที่อาจไม่ชัดเจนเกี่ยวกับเรื่องนั้น?"

คุณถูกต้อง ... ยกเว้นในวิธีที่คณะกรรมการมาตรฐาน C ++ อธิบายเรื่องนี้

หากผลข้างเคียงของวัตถุสเกลาร์นั้นสัมพันธ์กันกับผลข้างเคียงอื่นในวัตถุสเกลาร์เดียวกันพฤติกรรมนั้นจะไม่ได้กำหนดไว้

พวกเขาสามารถสร้างข้อยกเว้นพิเศษสำหรับกรณีพิเศษของคุณ แต่พวกเขาไม่ได้ทำ (และทำไมพวกเขาควรจะอะไรจะใช้ที่เคยอาจจะมี?) ดังนั้นจะยังคงเป็นi 5หรือฮาร์ดไดรฟ์ของคุณอาจว่างเปล่า ดังนั้นคำตอบสำหรับคำถามของคุณคือ:

มันเป็นพฤติกรรมที่ไม่ได้กำหนดเพราะมันไม่ได้กำหนดว่าพฤติกรรมคืออะไร

(สิ่งนี้สมควรได้รับการเน้นเพราะโปรแกรมเมอร์หลายคนคิดว่า "ไม่ได้กำหนด" หมายถึง "สุ่ม" หรือ "ไม่สามารถคาดเดาได้" มันไม่ได้; มันหมายถึงไม่ได้กำหนดโดยมาตรฐานพฤติกรรมอาจสอดคล้องกัน 100% และยังไม่ได้กำหนด)

มันสามารถถูกกำหนดพฤติกรรม? ใช่. มันถูกกำหนดไว้? ไม่ดังนั้นมันคือ "undefined"

ที่กล่าวว่า "ไม่ได้กำหนด" ไม่ได้หมายความว่าคอมไพเลอร์จะฟอร์แมตฮาร์ดไดรฟ์ของคุณ ... มันหมายความว่าทำได้และยังคงเป็นคอมไพเลอร์ที่ได้มาตรฐาน ฉันมั่นใจว่า g ++, Clang และ MSVC จะทำสิ่งที่คุณคาดหวัง พวกเขาแค่จะไม่ "ต้อง"


คำถามอื่นอาจเป็นสาเหตุที่คณะกรรมการมาตรฐาน C ++ เลือกที่จะทำให้ผลข้างเคียงนี้ไม่เกิดขึ้น . คำตอบนั้นจะเกี่ยวข้องกับประวัติและความคิดเห็นของคณะกรรมการ หรืออะไรคือสิ่งที่ดีเกี่ยวกับการเกิดผลข้างเคียงนี้ใน C ++? ซึ่งอนุญาตให้มีเหตุผลใด ๆ ไม่ว่าจะเป็นเหตุผลที่แท้จริงของคณะกรรมการมาตรฐานหรือไม่ คุณสามารถถามคำถามเหล่านั้นได้ที่นี่หรือที่ programmers.stackexchange.com


9
@hvd จริง ๆ แล้วฉันรู้ว่าถ้าคุณเปิดใช้-Wsequence-pointงาน g ++ มันจะเตือนคุณ
พอลเดรเปอร์

47
"ฉันแน่ใจว่า g ++, Clang และ MSVC จะทำสิ่งที่คุณคาดหวัง" ฉันจะไม่ไว้ใจคอมไพเลอร์สมัยใหม่ พวกมันชั่วร้าย ตัวอย่างเช่นพวกเขาอาจตระหนักว่านี่เป็นพฤติกรรมที่ไม่ได้กำหนดและถือว่ารหัสนี้ไม่สามารถเข้าถึงได้ หากพวกเขาไม่ทำวันนี้พวกเขาอาจจะทำในวันพรุ่งนี้ UB ใด ๆ เป็นระเบิดเวลาฟ้อง
CodesInChaos

8
@ BlacklightShining "คำตอบของคุณไม่ดีเพราะมันไม่ดี" ไม่ใช่คำติชมที่มีประโยชน์มากใช่ไหม?
Vincent van der Weele

13
@BobJarvis คอมไพล์ไม่มีข้อผูกมัดในการสร้างโค้ดที่ถูกต้องจากระยะไกลแม้ในกรณีที่มีพฤติกรรมที่ไม่ได้กำหนด มันสามารถสันนิษฐานได้ว่ารหัสนี้ไม่เคยถูกเรียกและแทนที่สิ่งทั้งหมดด้วย nop (โปรดทราบว่าคอมไพเลอร์จริง ๆ แล้วตั้งสมมติฐานเช่นนี้ต่อหน้า UB) ดังนั้นผมบอกว่าปฏิกิริยาที่ถูกต้องเช่นรายงานข้อผิดพลาดสามารถเป็น "ปิดการทำงานตามที่ตั้งใจไว้"
Grizzly

7
@SchighSchagh บางครั้งการใช้ถ้อยคำใหม่ (ซึ่งบนพื้นผิวดูเหมือนจะเป็นคำตอบที่ซ้ำซาก) เป็นสิ่งที่ผู้คนต้องการ คนส่วนใหญ่ที่ยังใหม่ต่อข้อกำหนดทางเทคนิคคิดว่าundefined behaviorหมายถึงsomething random will happenซึ่งอยู่ไกลจากกรณีส่วนใหญ่
Izkata

27

เหตุผลในทางปฏิบัติที่จะไม่ทำการยกเว้นจากกฎเพียงเพราะค่าทั้งสองเหมือนกัน:

// config.h
#define VALUEA  1

// defaults.h
#define VALUEB  1

// prog.cpp
f(i = VALUEA, i = VALUEB);

พิจารณากรณีนี้ได้รับอนุญาต

ตอนนี้หลายเดือนต่อมาความต้องการที่เกิดขึ้นมีการเปลี่ยนแปลง

 #define VALUEB 2

ดูเหมือนจะไม่เป็นอันตรายใช่มั้ย และทันใดนั้น prog.cpp ก็จะไม่รวบรวมอีกต่อไป แต่เรารู้สึกว่าการสะสมไม่ควรขึ้นอยู่กับคุณค่าของตัวอักษร

Bottom line: ไม่มีข้อยกเว้นกฎเพราะจะทำให้การรวบรวมที่ประสบความสำเร็จขึ้นอยู่กับค่า (ค่อนข้างประเภท) ของค่าคงที่

แก้ไข

@HeartWare ชี้ให้เห็นว่าการแสดงออกของฟอร์มอย่างต่อเนื่องA DIV Bไม่ได้รับอนุญาตในบางภาษาเมื่อBเป็น 0 และทำให้การรวบรวมล้มเหลว ดังนั้นการเปลี่ยนค่าคงที่อาจทำให้เกิดข้อผิดพลาดในการรวบรวมในที่อื่น นั่นคือ IMHO โชคร้าย แต่ก็เป็นเรื่องดีที่จะ จำกัด สิ่งต่าง ๆ เหล่านี้ไว้อย่างหลีกเลี่ยงไม่ได้


แน่นอน แต่ตัวอย่างไม่อักษรใช้จำนวนเต็ม คุณf(i = VALUEA, i = VALUEB);มีศักยภาพอย่างแน่นอนสำหรับพฤติกรรมที่ไม่ได้กำหนด ฉันหวังว่าคุณจะไม่ได้เข้ารหัสค่าที่อยู่เบื้องหลังตัวระบุจริงๆ
Wolf

3
@old แต่คอมไพเลอร์ไม่เห็นมาโครตัวประมวลผลล่วงหน้า และแม้ว่าสิ่งนี้จะไม่เป็นเช่นนั้นมันก็ยากที่จะหาตัวอย่างในภาษาการเขียนโปรแกรมใด ๆ ที่ซอร์สโค้ดรวบรวมจนกระทั่งมีการเปลี่ยนแปลงค่าคงที่ int จาก 1 เป็น 2 นี่เป็นเพียงการยอมรับและอธิบายไม่ได้ในขณะที่คุณเห็นคำอธิบายที่ดีมาก สาเหตุที่รหัสนั้นเสียแม้จะมีค่าเดียวกัน
Ingo

ใช่คอมไพล์ไม่เห็นมาโคร แต่นี่เป็นคำถามหรือไม่
Wolf

1
คำตอบของคุณหายไปจากจุดอ่านคำตอบที่เป็นอันตรายและความคิดเห็นของ OP เกี่ยวกับมัน
Wolf

1
SomeProcedure(A, B, B DIV (2-A))มันจะทำ อย่างไรก็ตามหากภาษาระบุว่า CONST ต้องได้รับการประเมินอย่างสมบูรณ์ในเวลารวบรวมดังนั้นแน่นอนการอ้างสิทธิ์ของฉันไม่ถูกต้องสำหรับกรณีนั้น เพราะมันทำให้ความแตกต่างของการรวบรวมและรันไทม์พร่ามัว มันจะสังเกตได้ไหมถ้าเราเขียนCONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y; ? หรือฟังก์ชั่นไม่ได้รับอนุญาต?
Ingo

12

ความสับสนก็คือการเก็บค่าคงที่ไว้ในตัวแปรท้องถิ่นไม่ใช่คำสั่งอะตอมมิกในทุก ๆ สถาปัตยกรรมที่ C ถูกออกแบบมาให้ทำงาน ตัวประมวลผลที่โค้ดทำงานนั้นมีความสำคัญมากกว่าคอมไพเลอร์ในกรณีนี้ ตัวอย่างเช่นบน ARM ที่แต่ละคำสั่งไม่สามารถดำเนินการค่าคงที่ 32 บิตทั้งหมดการจัดเก็บ int ในตัวแปรต้องการมากกว่าหนึ่งคำสั่ง ตัวอย่างด้วยรหัสเทียมนี้ที่คุณสามารถเก็บได้ครั้งละ 8 บิตเท่านั้นและต้องทำงานในการลงทะเบียน 32 บิตฉันเป็น int32:

reg = 0xFF; // first instruction
reg |= 0xFF00; // second
reg |= 0xFF0000; // third
reg |= 0xFF000000; // fourth
i = reg; // last

คุณสามารถจินตนาการได้ว่าถ้าคอมไพเลอร์ต้องการปรับให้เหมาะสมมันอาจแทรกลำดับเดียวกันสองครั้งและคุณไม่รู้ว่าค่าใดที่จะถูกเขียนไปยัง i; และสมมติว่าเขาไม่ฉลาดมาก:

reg = 0xFF;
reg |= 0xFF00;
reg |= 0xFF0000;
reg = 0xFF;
reg |= 0xFF000000;
i = reg; // writes 0xFF0000FF == -16776961
reg |= 0xFF00;
reg |= 0xFF0000;
reg |= 0xFF000000;
i = reg; // writes 0xFFFFFFFF == -1

อย่างไรก็ตามในการทดสอบของฉัน gcc นั้นดีพอที่จะรับรู้ว่ามีการใช้ค่าเดียวกันสองครั้งและสร้างครั้งเดียวและไม่ทำอะไรแปลก ๆ ฉันได้รับ -1, -1 แต่ตัวอย่างของฉันยังคงใช้ได้เนื่องจากเป็นสิ่งสำคัญที่จะต้องพิจารณาว่าแม้ค่าคงที่อาจไม่ชัดเจนเท่าที่ควร


ฉันคิดว่าบน ARM คอมไพเลอร์จะโหลดค่าคงที่จากตาราง สิ่งที่คุณอธิบายดูเหมือน MIPS มากกว่า
ACH

1
@AndreyChernyakhovskiy อ๋อ แต่ในกรณีที่มันไม่ใช่แค่-1(คอมไพเลอร์เก็บไว้ที่ไหนสักแห่ง) แต่มันค่อนข้าง3^81 mod 2^32แต่คงที่จากนั้นคอมไพเลอร์อาจทำสิ่งที่ทำที่นี่และในบางส่วนของ omtimization ฉัน interleave ลำดับการโทร เพื่อหลีกเลี่ยงการรอ
yo

@tohecz ใช่ฉันตรวจสอบแล้ว อันที่จริงคอมไพเลอร์ฉลาดเกินกว่าที่จะโหลดค่าคงที่ทุกอันจากตาราง อย่างไรก็ตามมันจะไม่ใช้การลงทะเบียนเดียวกันเพื่อคำนวณค่าคงที่ทั้งสอง สิ่งนี้จะ 'ยกเลิกการกำหนด' พฤติกรรมที่กำหนดอย่างแน่นอน
ACH

@AndreyChernyakhovskiy แต่คุณอาจไม่ใช่ "โปรแกรมเมอร์คอมไพเลอร์ C ++ ทุกคนในโลก" โปรดจำไว้ว่ามีเครื่องที่มีการลงทะเบียนแบบสั้น 3 รายการสำหรับการคำนวณเท่านั้น
yo

@tohecz พิจารณาตัวอย่างf(i = A, j = B)ที่iและjมีวัตถุทั้งสองแยกจากกัน ตัวอย่างนี้ไม่มี UB เครื่องที่มีการลงทะเบียนแบบสั้น 3 รายการเป็นข้อแก้ตัวสำหรับคอมไพเลอร์เพื่อผสมสองค่าของAและBในการลงทะเบียนเดียวกัน (ดังแสดงในคำตอบของ @ davidf) เพราะมันจะทำลายความหมายของโปรแกรม
ACH

11

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

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

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

uint8_t v;  // Global

void hey(uint8_t *p)
{
  moo(v=5, (*p)=6);
  zoo(v);
  zoo(v);
}

ถ้าคอมไพเลอร์ in-line เรียกไปที่ "moo" และสามารถบอกได้ว่ามันไม่ได้แก้ไข "v" มันอาจเก็บ 5 ถึง v จากนั้นเก็บ 6 ถึง * p จากนั้นผ่าน 5 ถึง "สวนสัตว์" แล้ว ส่งเนื้อหาของ v ไปที่ "สวนสัตว์" หาก "สวนสัตว์" ไม่ได้แก้ไข "v" ไม่ควรมีวิธีที่สองสายจะถูกส่งผ่านค่าที่แตกต่างกัน แต่ที่อาจเกิดขึ้นได้อย่างง่ายดาย ในทางตรงกันข้ามในกรณีที่ร้านค้าทั้งสองจะเขียนค่าเดียวกันความแปลกประหลาดนี้ไม่สามารถเกิดขึ้นได้และบนแพลตฟอร์มส่วนใหญ่จะไม่มีเหตุผลที่สมเหตุสมผลสำหรับการใช้งานเพื่อทำอะไรแปลก ๆ น่าเสียดายที่นักเขียนคอมไพเลอร์บางคนไม่ต้องการข้ออ้างใด ๆ สำหรับพฤติกรรมโง่ ๆ เกินกว่า "เพราะมาตรฐานอนุญาต" ดังนั้นแม้กรณีเหล่านั้นก็ไม่ปลอดภัย


9

ความจริงที่ว่าผลลัพธ์จะเหมือนกันในการใช้งานส่วนใหญ่ในกรณีนี้คือความบังเอิญ ลำดับการประเมินยังคงไม่ได้กำหนด พิจารณาf(i = -1, i = -2): ที่นี่เรื่องการสั่งซื้อ -1เหตุผลเดียวที่มันไม่สำคัญว่าในตัวอย่างของคุณเป็นอุบัติเหตุที่ค่าทั้งสอง

เนื่องจากนิพจน์นั้นถูกระบุว่าเป็นหนึ่งเดียวกับพฤติกรรมที่ไม่ได้กำหนดคอมไพเลอร์ที่เข้ากันได้กับประสงค์ร้ายอาจแสดงภาพที่ไม่เหมาะสมเมื่อคุณประเมินf(i = -1, i = -1)และยกเลิกการดำเนินการ - และยังถือว่าถูกต้องสมบูรณ์ โชคดีที่ไม่มีคอมไพเลอร์ที่ฉันรู้


8

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

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

สิ่งนี้ไม่ได้กำหนดการเรียงลำดับระหว่างนิพจน์อาร์กิวเมนต์ดังนั้นเราจึงสิ้นสุดในกรณีนี้:

1) หากผลข้างเคียงของวัตถุสเกลาร์นั้นสัมพันธ์กันกับผลข้างเคียงอื่นของวัตถุสเกลาร์เดียวกันพฤติกรรมนั้นจะไม่ได้กำหนดไว้

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

void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}

8

C ++ 17กำหนดกฎการประเมินที่เข้มงวด โดยเฉพาะอย่างยิ่งมันเรียงลำดับฟังก์ชันอาร์กิวเมนต์ (แม้ว่าจะไม่ได้ระบุไว้)

N5659 §4.6:15
การประเมินAและBจะถูกจัดลำดับตามลำดับเมื่อAถูกจัดลำดับก่อนที่BหรือBจะถูกจัดลำดับก่อนAแต่จะไม่ได้ระบุไว้ [ หมายเหตุ : การประเมินตามลำดับที่กำหนดไม่สามารถซ้อนทับกัน แต่สามารถดำเนินการก่อนได้ - หมายเหตุท้าย ]

N5659 § 8.2.2:5
การกำหนดค่าเริ่มต้นของพารามิเตอร์รวมถึงการคำนวณค่าที่เกี่ยวข้องและผลข้างเคียงทั้งหมดจะถูกจัดลำดับตามลำดับโดยไม่เกี่ยวข้องกับพารามิเตอร์อื่น ๆ

อนุญาตให้บางกรณีซึ่งจะเป็น UB ก่อนหน้า:

f(i = -1, i = -1); // value of i is -1
f(i = -1, i = -2); // value of i is either -1 or -2, but not specified which one

2
ขอบคุณที่เพิ่มการอัปเดตนี้สำหรับc ++ 17ดังนั้นฉันจึงไม่ต้องทำ ;)
Yakk - Adam Nevraumont

เยี่ยมมากขอบคุณมากสำหรับคำตอบนี้ การติดตามเล็กน้อย: ถ้าfเป็นลายเซ็นf(int a, int b)C ++ 17 รับประกันได้หรือไม่a == -1และb == -2ในกรณีที่สอง
Nicu Stiurca

ใช่. ถ้าเรามีพารามิเตอร์aและbแล้วทั้งi-then- aจะเริ่มต้น -1 หลังจากนั้นi-then- bจะเริ่มต้นถึง -2 หรือทางรอบ ในทั้งสองกรณีเราจบลงด้วยและa == -1 b == -2อย่างน้อยนี่คือวิธีที่ฉันอ่าน " การกำหนดค่าเริ่มต้นของพารามิเตอร์รวมถึงการคำนวณค่าที่เกี่ยวข้องและผลข้างเคียงทั้งหมดจะถูกจัดลำดับตามลำดับโดยไม่เกี่ยวข้องกับพารามิเตอร์อื่น ๆ "
AlexD

ฉันคิดว่ามันคงเหมือนเดิมใน C มาตลอดกาล
fuz

5

ผู้ประกอบการที่ได้รับมอบหมายสามารถโอเวอร์โหลดในกรณีนี้คำสั่งซื้ออาจมีความสำคัญ:

struct A {
    bool first;
    A () : first (false) {
    }
    const A & operator = (int i) {
        first = !first;
        return * this;
    }
};

void f (A a1, A a2) {
    // ...
}


// ...
A i;
f (i = -1, i = -1);   // the argument evaluated first has ax.first == true

1
จริงพอ แต่คำถามเกี่ยวกับประเภทสเกลาร์ที่คนอื่น ๆ ได้ชี้ให้เห็นหมายถึงครอบครัว int ครอบครัวลอยครอบครัวและพอยน์เตอร์
Nicu Stiurca

ปัญหาที่แท้จริงในกรณีนี้คือตัวดำเนินการกำหนดค่าเป็น stateful ดังนั้นแม้แต่การจัดการตัวแปรอย่างสม่ำเสมอก็มีแนวโน้มที่จะเกิดปัญหาเช่นนี้
AJMansfield

2

นี่เป็นเพียงการตอบ "ฉันไม่แน่ใจว่า" วัตถุเซนต์คิตส์และเนวิส "อาจหมายถึงอะไรนอกเหนือจากสิ่งที่เหมือน int หรือลอย"

ฉันจะตีความ "วัตถุสเกลาร์" เป็นตัวย่อของ "วัตถุประเภทสเกลาร์" หรือเพียงแค่ "ตัวแปรสเกลาร์ชนิด" จากนั้นpointer,enum (คงที่) เป็นประเภทเกลา

นี่เป็นบทความ MSDN ของประเภทสเกลาร์


สิ่งนี้อ่านเหมือน "ลิงก์คำตอบเท่านั้น" คุณสามารถคัดลอกบิตที่เกี่ยวข้องจากลิงก์นั้นไปยังคำตอบนี้ (ใน blockquote) ได้ไหม?
โคลจอห์นสัน

1
@ColeJohnson นี่ไม่ใช่ลิงก์คำตอบเท่านั้น ลิงค์นี้มีไว้สำหรับอธิบายเพิ่มเติมเท่านั้น คำตอบของฉันคือ "ตัวชี้", "enum"
Peng Zhang

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

1

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

void g(int a, int b, int c, int n) {
    int i;
    // hey, compiler has to prove Fermat's theorem now!
    f(i = 1, i = (ipow(a, n) + ipow(b, n) == ipow(c, n)));
}

1
ไม่จำเป็นต้องพิสูจน์ทฤษฎีบทของแฟร์มาต์: เพียงแค่กำหนดที่จะ1 iทั้งอาร์กิวเมนต์กำหนด1และสิ่งนี้จะทำสิ่ง "ถูกต้อง" หรืออาร์กิวเมนต์กำหนดค่าที่แตกต่างกันและมันเป็นพฤติกรรมที่ไม่ได้กำหนดดังนั้นทางเลือกของเรายังคงได้รับอนุญาต
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.