ในทางปฏิบัติทำไมคอมไพเลอร์ที่ต่างกันจึงคำนวณค่า int x = ++ i + ++ i ต่างกัน?


165

พิจารณารหัสนี้:

int i = 1;
int x = ++i + ++i;

เรามีการคาดเดาว่าคอมไพเลอร์อาจทำอะไรกับโค้ดนี้โดยสมมติว่าคอมไพเลอร์

  1. ทั้ง++iผลตอบแทนที่เกิดใน2x=4
  2. หนึ่งใน++iผลตอบแทน2และผลตอบแทนอื่น ๆ ที่มีผลในการ3x=5
  3. ทั้ง++iผลตอบแทนที่เกิดใน3x=6

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

แต่ผมวิ่งรหัสนี้ใน Visual Studio 6และผลที่ได้ก็คือ ฉันพยายามทำความเข้าใจคอมไพเลอร์ให้ดีขึ้นและฉันสงสัยว่าอะไรอาจนำไปสู่ผลลัพธ์ของ6ไฟล์. ฉันเดาเพียงอย่างเดียวคือโค้ดสามารถทำงานพร้อมกัน "ในตัว" บางอย่างได้ สอง++ผู้ประกอบการถูกเรียกว่าแต่ละคนเพิ่มขึ้นก่อนที่จะกลับมาอีกแล้วพวกเขาทั้งสองกลับi 3สิ่งนี้จะขัดแย้งกับความเข้าใจของฉันเกี่ยวกับ call stack และจำเป็นต้องอธิบายออกไป

สิ่งใด (ที่สมเหตุสมผล) ที่C++คอมไพเลอร์สามารถทำได้ซึ่งจะนำไปสู่ผลลัพธ์4หรือผลลัพธ์หรือ6?

บันทึก

ตัวอย่างนี้เป็นตัวอย่างของพฤติกรรมที่ไม่ได้กำหนดไว้ในการเขียนโปรแกรมของ Bjarne Stroustrup: หลักการและการปฏิบัติโดยใช้ C ++ (C ++ 14)

ดูความคิดเห็นของอบเชย


5
ข้อมูลจำเพาะ C ไม่ได้ครอบคลุมลำดับของการดำเนินการหรือการประเมินทางด้านขวาของ = เมื่อเทียบกับการดำเนินการก่อน / หลังการเพิ่มขึ้นทางด้านซ้ายเท่านั้น
Cristobol Polychronopolis

2
แนะนำให้คุณอ้างอิงในคำถามหากคุณได้ตัวอย่างนี้จากหนังสือของ Stroustrup (ตามที่กล่าวไว้ในความคิดเห็นของคำตอบข้อใดข้อหนึ่ง)
Daniel

4
@philipxy คำแนะนำของคุณไม่ซ้ำกับคำถามนี้ คำถามจะแตกต่างกัน คำตอบในรายการซ้ำที่คุณแนะนำไม่ตอบคำถามนี้ คำตอบในคำตอบที่แนะนำของคุณไม่ซ้ำกับคำตอบที่ได้รับการยอมรับ (หรือคะแนนโหวตสูง) สำหรับคำถามนี้ ฉันเชื่อว่าคุณอ่านคำถามของฉันผิด ฉันขอแนะนำให้คุณอ่านซ้ำและพิจารณาการโหวตใหม่เพื่อปิด
อบเชย

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

2
เพื่อตอบชื่อ; เนื่องจาก UB หมายถึง bahavioir ไม่ได้กำหนดไว้ คอมไพเลอร์หลายตัวที่สร้างขึ้นในช่วงเวลาที่แตกต่างกันตลอดประวัติศาสตร์โดยผู้คนที่แตกต่างกันสำหรับสถาปัตยกรรมที่แตกต่างกันเมื่อถูกขอให้ใช้สีนอกเส้นและนำไปใช้งานจริงพวกเขาต้องใส่บางสิ่งในส่วนนี้นอกข้อกำหนดดังนั้นผู้คนจึงทำอย่างนั้นและแต่ละอย่าง พวกเขาใช้ดินสอสีที่แตกต่างกัน ดังนั้นพวก hoary maxim อย่าพึ่ง UB
Toby

คำตอบ:


200

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

รหัส

int i = 1;
int x = ++i + ++i;

ประกอบด้วยคำแนะนำต่อไปนี้:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

แต่ถึงแม้จะเป็นรายการลำดับเลขตามที่ฉันเขียน แต่ก็มีการอ้างอิงการสั่งซื้อเพียงไม่กี่รายการที่นี่: 1-> 2-> 3-> 4-> 5-> 10-> 11 และ 1-> 6-> 7- > 8-> 9-> 10-> 11 ต้องอยู่ตามลำดับญาติ นอกเหนือจากนั้นคอมไพลเลอร์สามารถจัดลำดับใหม่ได้อย่างอิสระและอาจกำจัดความซ้ำซ้อน

ตัวอย่างเช่นคุณสามารถจัดลำดับรายการดังนี้:

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
4. store tmp1 in i
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

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

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

และตอนนี้ทุกอย่างที่เกี่ยวข้องกับ tmp1 เป็นรหัสตาย: ไม่เคยใช้ และการอ่านซ้ำของฉันก็สามารถกำจัดได้เช่นกัน:

1. store 1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
10. add tmp3 and tmp3, as tmp5
11. store tmp5 in x

ดูรหัสนี้จะสั้นกว่ามาก เครื่องมือเพิ่มประสิทธิภาพมีความสุข โปรแกรมเมอร์ไม่ได้เป็นเพราะฉันเพิ่มขึ้นเพียงครั้งเดียว อ๊ะ.

ลองดูอย่างอื่นที่คอมไพเลอร์สามารถทำได้แทน: กลับไปใช้เวอร์ชันดั้งเดิมกันเถอะ

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

คอมไพเลอร์สามารถจัดลำดับใหม่ได้ดังนี้:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

แล้วสังเกตอีกครั้งว่าฉันอ่านสองครั้งดังนั้นให้กำจัดหนึ่งในนั้น:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

เป็นสิ่งที่ดี แต่สามารถไปได้ไกลกว่านี้: สามารถใช้ tmp1 ซ้ำได้:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

จากนั้นสามารถกำจัดการอ่านซ้ำของ i ใน 6:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

ตอนนี้ 4 เป็นร้านค้าที่ตายแล้ว:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

และตอนนี้ 3 และ 7 สามารถรวมเป็นคำสั่งเดียว:

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

กำจัดชั่วคราวสุดท้าย:

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
10. add tmp1 and tmp1, as tmp5
11. store tmp5 in x

และตอนนี้คุณได้รับผลลัพธ์ที่ Visual C ++ มอบให้คุณแล้ว

โปรดทราบว่าในทั้งสองเส้นทางการเพิ่มประสิทธิภาพการอ้างอิงลำดับที่สำคัญจะถูกเก็บรักษาไว้ตราบเท่าที่คำแนะนำไม่ได้ถูกลบออกเพื่อไม่ทำอะไรเลย


36
ปัจจุบันนี้เป็นคำตอบเดียวที่กล่าวถึงการจัดลำดับ
PM 2Ring

3
-1 ฉันไม่คิดว่าคำตอบนี้จะชัดเจน ผลลัพธ์ที่สังเกตไม่ได้ขึ้นอยู่กับการเพิ่มประสิทธิภาพคอมไพเลอร์ใด ๆ เลย (ดูคำตอบของฉัน)
Daniel

3
สิ่งนี้ถือว่าเป็นการดำเนินการอ่านแก้ไข - เขียน ซีพียูบางตัวเช่น x86 ที่แพร่หลายมีการดำเนินการเพิ่มระดับอะตอมซึ่งจะเพิ่มความซับซ้อนให้กับสถานการณ์มากยิ่งขึ้น
Mark

6
@philipxy "มาตรฐานไม่มีอะไรจะพูดเกี่ยวกับออบเจ็กต์โค้ด" มาตรฐานไม่มีอะไรจะพูดเกี่ยวกับพฤติกรรมของตัวอย่างข้อมูลนี้เช่นกันนั่นคือ UB นั่นเป็นหลักฐานของคำถาม OP ต้องการทราบว่าทำไมในทางปฏิบัติคอมไพเลอร์ถึงได้ผลลัพธ์ที่แตกต่างและแปลกประหลาด นอกจากนี้คำตอบของฉันไม่ได้พูดอะไรเกี่ยวกับรหัสวัตถุ
Sebastian Redl

5
@philipxy ฉันไม่เข้าใจคำคัดค้านของคุณ ตามที่ระบุไว้คำถามคือสิ่งที่คอมไพเลอร์อาจทำต่อหน้า UB ไม่ใช่เกี่ยวกับมาตรฐาน C ++ เหตุใดการใช้รหัสออบเจ็กต์จึงไม่เหมาะสมเมื่อสำรวจว่าคอมไพเลอร์สมมุติเปลี่ยนรหัสอย่างไร ในความเป็นจริงแล้วอะไรก็ตามนอกจากรหัสวัตถุจะเกี่ยวข้องกันอย่างไร?
Konrad Rudolph

58

แม้ว่านี่จะเป็น UB (ตามที่ OP โดยนัย) ต่อไปนี้เป็นวิธีสมมติที่คอมไพเลอร์จะได้ผลลัพธ์ 3 ทั้งสามจะให้ถูกต้องเหมือนกันxผลถ้าใช้กับที่แตกต่างกันตัวแปรแทนหนึ่งเดียวกันint i = 1, j = 1;i

  1. ทั้ง ++ ฉันส่งคืน 2 ผลลัพธ์เป็น x = 4
int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4
  1. หนึ่ง ++ ฉันส่งคืน 2 และอีกตัวส่งคืน 3 ทำให้ x = 5
int i = 1;
int i1 = ++i;           // i1 = 2
int i2 = ++i;           // i2 = 3
int x = i1 + i2;        // x = 5
  1. ทั้ง ++ ฉันส่งคืน 3 ทำให้ x = 6
int i = 1;
int &i1 = i, &i2 = i;
++i1;                   // i = 2
++i2;                   // i = 3
int x = i1 + i2;        // x = 6

2
นี่เป็นการตอบรับที่ดีกว่าที่หวังไว้ขอบคุณ
อบเชย

1
สำหรับตัวเลือกที่ 1 คอมไพเลอร์อาจจะมีการทำบันทึกการ ipreincrement การรู้ว่ามันเกิดขึ้นได้เพียงครั้งเดียวมันจะเปล่งออกมาเพียงครั้งเดียวเท่านั้น สำหรับตัวเลือกที่ 2 รหัสจะถูกแปลเป็นรหัสเครื่องตามตัวอักษรเช่นเดียวกับโครงการคลาสคอมไพเลอร์ของวิทยาลัยอาจทำได้ สำหรับตัวเลือกที่ 3 ก็เหมือนกับตัวเลือกที่ 1 แต่ทำสำเนาไว้ล่วงหน้าสองชุด ต้องใช้เวกเตอร์ไม่ใช่เซต :-)
Zan Lynx

@dxiv ขอโทษฉันไม่ดีฉันผสมโพสต์
muru

22

สำหรับฉันอย่างที่สองดูเหมือนจะเป็นไปได้มากที่สุด

ฉันกำลังเลือกตัวเลือก # 4: ทั้งสองอย่าง ++iเกิดขึ้นพร้อมกัน

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

ฉันสามารถเห็นสภาพการแข่งขันที่ก่อให้เกิดพฤติกรรมที่ไม่ได้กำหนดหรือความผิดพลาดของบัสเนื่องจากการขัดแย้งของหน่วยความจำเดียวกัน - ทั้งหมดได้รับอนุญาตเนื่องจาก coder ละเมิดสัญญา C ++ ดังนั้น UB

คำถามของฉันคือสิ่งที่ (สมเหตุสมผล) ที่คอมไพเลอร์ C ++ สามารถทำสิ่งนั้นจะนำไปสู่ผลลัพธ์ของ 4 หรือผลลัพธ์หรือ 6

มันทำได้แต่อย่านับรวมในนั้น

อย่าใช้++i + ++iหรือคาดหวังผลลัพธ์ที่สมเหตุสมผล


ถ้าฉันสามารถยอมรับทั้งคำตอบนี้และของ @ dxiv ฉันจะทำ ขอบคุณสำหรับการตอบรับ
อบเชย

4
@UriRaz: โปรเซสเซอร์อาจไม่สังเกตว่ามีอันตรายของข้อมูลขึ้นอยู่กับตัวเลือกของคอมไพเลอร์ เช่นคอมไพลเลอร์อาจกำหนดให้iกับรีจิสเตอร์สองตัวเพิ่มรีจิสเตอร์ทั้งคู่และเขียนทั้งสองรีจิสเตอร์ โปรเซสเซอร์ไม่มีทางแก้ไขได้ ปัญหาพื้นฐานคือ C ++ และ CPU ที่ทันสมัยไม่ได้เป็นลำดับอย่างเคร่งครัด C ++ มีลำดับเหตุการณ์ก่อนและเกิดหลังอย่างชัดเจนเพื่อให้เกิดขึ้นในเวลาเดียวกันตามค่าเริ่มต้น
MSalters

1
แต่เรารู้ว่านั่นไม่ใช่กรณีของ OP ที่ใช้ Visual Studio ISAs หลักส่วนใหญ่รวมถึง x86 และ ARM ถูกกำหนดในรูปแบบของการดำเนินการตามลำดับโดยสมบูรณ์ซึ่งคำสั่งเครื่องหนึ่งจะเสร็จสมบูรณ์ก่อนที่จะเริ่มต้นครั้งต่อไป Superscalar ที่ไม่ได้รับคำสั่งต้องคงไว้ซึ่งภาพลวงตานั้นสำหรับเธรดเดียว (เธรดอื่น ๆ ที่อ่านหน่วยความจำที่ใช้ร่วมกันไม่รับประกันว่าจะเห็นสิ่งต่างๆตามลำดับโปรแกรม แต่กฎสำคัญของผู้บริหาร OoO คือไม่ทำลายการดำเนินการแบบเธรดเดียว)
Peter Cordes

1
นี่คือคำตอบที่ฉันชอบเพราะเป็นคำตอบเดียวที่กล่าวถึงการดำเนินการคำสั่งแบบขนานที่ระดับ CPU Btw จะเป็นการดีที่จะกล่าวถึงในคำตอบว่าเนื่องจากเธรด cpu เงื่อนไขการแข่งขันจะหยุดรอการปลดล็อก mutex ในตำแหน่งหน่วยความจำเดียวกันดังนั้นนี่จึงไม่เหมาะสมอย่างยิ่งในรูปแบบการทำงานพร้อมกัน ประการที่สอง - เนื่องจากเงื่อนไขการแข่งขันเดียวกันคำตอบจริงอาจเป็นได้4หรือ5- ขึ้นอยู่กับรูปแบบการดำเนินการเธรด cpu / ความเร็วดังนั้นนี่จึงเป็นหัวใจสำคัญของ UB
Agnius Vasiliauskas

1
@AgniusVasiliauskas บางที "ในทางปฏิบัติทำไมคอมไพเลอร์ที่แตกต่างกันจึงคำนวณค่าที่แตกต่างกัน" กำลังมองหาสิ่งที่เข้าใจง่ายมากขึ้นเนื่องจากมุมมองที่เรียบง่ายของโปรเซสเซอร์ในปัจจุบัน ยังมีช่วงของสถานการณ์จำลองของคอมไพเลอร์ / ตัวประมวลผลหากมากกว่าคำตอบต่างๆที่กล่าวถึง ข้อมูลเชิงลึกที่เป็นประโยชน์ของคุณยังเป็นอีก IMO ความเท่าเทียมกันคืออนาคตดังนั้นคำตอบนี้จึงมุ่งเน้นไปที่สิ่งเหล่านั้นแม้ว่าจะเป็นแบบนามธรรมก็ตาม - ในขณะที่อนาคตยังคงเปิดเผย IAC โพสต์ดังกล่าวได้รับความนิยมและง่ายต่อการเข้าใจคำตอบจะได้รับรางวัลที่ดีที่สุด
chux - คืนสถานะ Monica

17

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

  1. เพิ่มขึ้น i
  2. เพิ่มขึ้น i
  3. เพิ่มi+i

เมื่อiเพิ่มขึ้นสองครั้งค่าของมันจะเป็น 3 และเมื่อรวมเข้าด้วยกันผลรวมจะเป็น 6

สำหรับการตรวจสอบให้พิจารณาว่านี่เป็นฟังก์ชัน C ++:

int dblInc ()
{
    int i = 1;
    int x = ++i + ++i;
    return x;   
}

นี่คือรหัสแอสเซมบลีที่ฉันได้รับจากการรวบรวมฟังก์ชันนั้นโดยใช้คอมไพเลอร์ GNU C ++ เวอร์ชันเก่า (win32, gcc เวอร์ชัน 3.4.2 (mingw-special)) ไม่มีการเพิ่มประสิทธิภาพแบบแฟนซีหรือมัลติเธรดเกิดขึ้นที่นี่:

__Z6dblIncv:
    push    ebp
    mov ebp, esp
    sub esp, 8
    mov DWORD PTR [ebp-4], 1
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    mov eax, DWORD PTR [ebp-4]
    add eax, DWORD PTR [ebp-4]
    mov DWORD PTR [ebp-8], eax
    mov eax, DWORD PTR [ebp-8]
    leave
    ret

โปรดทราบว่าตัวแปรท้องถิ่นนั่งอยู่บนสแต็คในเวลาเพียงหนึ่งสถานที่เดียวที่อยู่i [ebp-4]ตำแหน่งนั้นจะเพิ่มขึ้นสองครั้ง (ในบรรทัดที่ 5-8 ของฟังก์ชันการประกอบรวมทั้งเห็นได้ชัดว่ามีการโหลดแอดเดรสนั้นซ้ำซ้อนeax) จากนั้นในบรรทัดที่ 9-10 ค่านั้นจะถูกโหลดเข้าไปeaxแล้วเพิ่มเข้าไปeax(นั่นคือคำนวณกระแสi + i) จากนั้นจะคัดลอกซ้ำซ้อนไปที่สแต็กและกลับไปที่eaxเป็นค่าส่งคืน (ซึ่งจะเห็นได้ชัดว่าเป็น 6)

อาจเป็นเรื่องที่น่าสนใจที่จะดูมาตรฐาน C ++ (ที่นี่เป็นมาตรฐานเก่า: ISO / IEC 14882: 1998 (E)) ซึ่งระบุไว้สำหรับนิพจน์ส่วน 5.4:

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

ด้วยเชิงอรรถ:

ไม่ได้ระบุลำดับความสำคัญของตัวดำเนินการโดยตรง แต่สามารถได้มาจากไวยากรณ์

ในจุดนั้นจะมีตัวอย่างพฤติกรรมที่ไม่ระบุสองตัวอย่างซึ่งทั้งสองอย่างเกี่ยวข้องกับตัวดำเนินการส่วนเพิ่ม (ซึ่งหนึ่งในนั้นคือi = ++i + 1:)

ตอนนี้ถ้าใครต้องการก็สามารถ: สร้างคลาส wrapper จำนวนเต็ม (เช่น Java Integer); ฟังก์ชันโอเวอร์โหลดoperator+และoperator++ส่งคืนอ็อบเจ็กต์ค่ากลาง และด้วยเหตุนี้จึงเขียน++iObj + ++iObjและรับมันเพื่อส่งคืนวัตถุที่ถือ 5 (ฉันไม่ได้รวมโค้ดเต็มไว้ที่นี่เพื่อความกะทัดรัด)

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


2
ตัวดำเนินการส่วนเพิ่มมีค่า "ผลตอบแทน" ที่กำหนดไว้เป็นอย่างดีจริงๆ
edc65

@philipxy: ฉันได้แก้ไขคำตอบเพื่อลบข้อความที่คุณคัดค้าน คุณอาจเห็นด้วยกับคำตอบเพิ่มเติม ณ จุดนี้
Daniel

2
สิ่งเหล่านี้ไม่ใช่ "สองตัวอย่างของพฤติกรรมที่ไม่ระบุรายละเอียด" ซึ่งเป็นสองตัวอย่างของพฤติกรรมที่ไม่ได้กำหนดซึ่งเป็นสัตว์ร้ายที่แตกต่างกันมากซึ่งเกิดจากข้อความที่แตกต่างกันในมาตรฐาน ฉันเห็น C ++ 98 เคยพูดว่า "ไม่ระบุ" ในข้อความของตัวอย่างเชิงอรรถซึ่งขัดแย้งกับข้อความเชิงบรรทัดฐาน แต่จะได้รับการแก้ไขในภายหลัง
Cubbi

@Cubbi: ทั้งข้อความและเชิงอรรถในมาตรฐานที่ยกมานี้ใช้วลี "ไม่ระบุ", "ไม่ได้ระบุโดยตรง" และดูเหมือนว่าจะตรงกับคำที่มาจากคำจำกัดความส่วน 1.3.13
Daniel

1
@philipxy: ฉันเห็นว่าคุณเคยแสดงความคิดเห็นเดิมซ้ำกับคำตอบมากมายที่นี่ ดูเหมือนการวิจารณ์หลักของคุณจะเกี่ยวกับคำถามของ OP มากกว่าซึ่งขอบเขตของมันไม่ได้เป็นเพียงแค่มาตรฐานนามธรรมเท่านั้น
Daniel

7

สิ่งที่สมเหตุสมผลที่คอมไพเลอร์สามารถทำได้คือ Common Subexpression Elimination นี่เป็นการเพิ่มประสิทธิภาพทั่วไปในคอมไพเลอร์อยู่แล้ว: หากนิพจน์ย่อยเช่น(x+1)เกิดขึ้นมากกว่าหนึ่งครั้งในนิพจน์ที่ใหญ่กว่าจะต้องคำนวณเพียงครั้งเดียว เช่นในa/(x+1) + b*(x+1)ไฟล์x+1ย่อยแสดงออกสามารถคำนวณได้ครั้งเดียว

แน่นอนว่าคอมไพลเลอร์ต้องรู้ว่านิพจน์ย่อยใดสามารถปรับให้เหมาะสมได้ด้วยวิธีนั้น การโทรrand()สองครั้งควรให้สองหมายเลขแบบสุ่ม การเรียกใช้ฟังก์ชันแบบไม่อินไลน์จึงต้องได้รับการยกเว้นจาก CSE ดังที่คุณทราบไม่มีกฎใดที่บอกว่าi++ควรจัดการเหตุการณ์ทั้งสองครั้งอย่างไรจึงไม่มีเหตุผลที่จะยกเว้นจาก CSE

ผลลัพธ์อาจint x = ++i + ++i;เป็นไปint __cse = i++; int x = __cse << 1ตามนั้น (CSE ตามด้วยการลดความแรงซ้ำ)


มาตรฐานไม่มีอะไรจะพูดเกี่ยวกับรหัสวัตถุ สิ่งนี้ไม่เป็นธรรมหรือเกี่ยวข้องกับคำจำกัดความของภาษา
philipxy

1
@philipxy: มาตรฐานไม่มีอะไรจะพูดเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดรูปแบบใด ๆ นั่นคือหลักฐานของคำถาม
MSalters

7

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

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

Downvoter: GCC ไม่เห็นด้วยกับคุณอย่างยิ่ง


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

6

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

เดี๋ยวนะ! ไม่ชัดเจนว่าจะต้องเกิดอะไรขึ้น? นอกจากนี้ความต้องการผลของการเพิ่มที่สองเพื่อให้เห็นได้ชัดว่าเหล่านี้จะต้องเกิดขึ้นเป็นครั้งแรก แล้วเราก็ไปซ้ายไปขวาเลย ... อ๊าก! ถ้ามันง่ายมาก โชคไม่ดีที่ไม่ใช่อย่างนั้น เราไม่เดินจากซ้ายไปขวานั่นคือปัญหา

การอ่านตำแหน่งหน่วยความจำเป็นสองรีจิสเตอร์ (หรือเริ่มต้นทั้งสองจากลิเทอรัลเดียวกันการเพิ่มประสิทธิภาพการเดินทางไปกลับหน่วยความจำ) เป็นสิ่งที่สมเหตุสมผลมากสำหรับคอมไพลเลอร์ที่จะทำ สิ่งนี้จะมีผลอย่างมีประสิทธิภาพจากการที่มีตัวแปรที่แตกต่างกันสองตัวแปรแต่ละตัวมีค่าเป็น 2 ซึ่งสุดท้ายจะถูกเพิ่มเข้าไปในผลลัพธ์ของ 4 สิ่งนี้ "สมเหตุสมผล" เพราะมันรวดเร็วและมีประสิทธิภาพและเป็นไปตามทั้งสองอย่างมาตรฐานและรหัส

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

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

คอมไพเลอร์เป็นโดยใช้ถ้อยคำของมาตรฐานอนุญาตให้ให้ผลลัพธ์ใด ๆ แก่คุณแม้ว่าฉันจะคิดว่า 6 บันทึก "fuck you" จากแผนก Obnoxious โดยส่วนตัวเนื่องจากเป็นสิ่งที่ไม่คาดคิด (ถูกกฎหมายหรือไม่ การพยายามให้เซอร์ไพรส์น้อยที่สุดอยู่เสมอเป็นสิ่งที่ควรทำ!) แม้ว่าจะเห็นว่าพฤติกรรมที่ไม่ได้กำหนดมีส่วนเกี่ยวข้องอย่างไร แต่น่าเศร้าที่เราไม่สามารถโต้แย้งเกี่ยวกับ "ไม่คาดคิด" ได้ใช่ไหม

แล้วโค้ดที่คุณมีอยู่ในคอมไพเลอร์คืออะไร? ขอเสียงดังซึ่งจะแสดงให้เราเห็นว่าเราถามอย่างดี (วิงวอนด้วย-ast-dump -fsyntax-only):

ast.cpp:4:9: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
int x = ++i + ++i;
        ^     ~~
(some lines omitted)
`-CompoundStmt 0x2b3e628 <line:2:1, line:5:1>
  |-DeclStmt 0x2b3e4b8 <line:3:1, col:10>
  | `-VarDecl 0x2b3e430 <col:1, col:9> col:5 used i 'int' cinit
  |   `-IntegerLiteral 0x2b3e498 <col:9> 'int' 1
  `-DeclStmt 0x2b3e610 <line:4:1, col:18>
    `-VarDecl 0x2b3e4e8 <col:1, col:17> col:5 x 'int' cinit
      `-BinaryOperator 0x2b3e5f0 <col:9, col:17> 'int' '+'
        |-ImplicitCastExpr 0x2b3e5c0 <col:9, col:11> 'int' <LValueToRValue>
        | `-UnaryOperator 0x2b3e570 <col:9, col:11> 'int' lvalue prefix '++'
        |   `-DeclRefExpr 0x2b3e550 <col:11> 'int' lvalue Var 0x2b3e430 'i' 'int'
        `-ImplicitCastExpr 0x2b3e5d8 <col:15, col:17> 'int' <LValueToRValue>
          `-UnaryOperator 0x2b3e5a8 <col:15, col:17> 'int' lvalue prefix '++'
            `-DeclRefExpr 0x2b3e588 <col:17> 'int' lvalue Var 0x2b3e430 'i' 'int'

อย่างที่คุณเห็นlvalue Var 0x2b3e430คำนำหน้าเหมือนกัน++ถูกนำไปใช้ในสถานที่สองแห่งและทั้งสองนี้อยู่ด้านล่างโหนดเดียวกันในแผนภูมิซึ่งเป็นตัวดำเนินการที่ไม่ใช่พิเศษ (+) ซึ่งไม่มีอะไรพิเศษที่กล่าวถึงเกี่ยวกับการจัดลำดับหรือสิ่งนั้น เหตุใดสิ่งนี้จึงสำคัญ อ่านต่อ

หมายเหตุคำเตือน: "การแก้ไข" i "โดยไม่ได้รับผลกระทบหลายรายการ โอ้ฟังดูไม่ดี หมายความว่าอย่างไร? [basic.exec]บอกเราเกี่ยวกับผลข้างเคียงและการจัดลำดับและจะบอกเรา (ย่อหน้าที่ 10) ว่าโดยค่าเริ่มต้นเว้นแต่จะกล่าวไว้อย่างชัดเจนการประเมินตัวถูกดำเนินการของแต่ละตัวดำเนินการและนิพจน์ย่อยของนิพจน์แต่ละรายการจะไม่เกิดขึ้นตามมา เอาละนั่นคือกรณีoperator+- ไม่มีการพูดเป็นอย่างอื่นดังนั้น ...

แต่เราสนใจเกี่ยวกับลำดับก่อนหลังไม่แน่นอนลำดับหรือไม่ตามมา? ใครอยากรู้ล่ะก็

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


การใช้ "สมเหตุสมผล" เป็นเพียงการห้ามไม่ให้ใครพูดว่า "คอมไพเลอร์สามารถทำอะไรก็ได้แม้กระทั่งปล่อยคำสั่งเดียว 'set x ถึง 7'" บางทีฉันควรจะชี้แจง
อบเชย

@cinnamon หลายปีก่อนตอนที่ฉันยังเด็กและไม่มีประสบการณ์วิศวกรคอมไพเลอร์ของ Sun บอกฉันว่าคอมไพเลอร์ของพวกเขาทำหน้าที่ผลิตโค้ดที่สมเหตุสมผลอย่างยิ่งสำหรับพฤติกรรมที่ไม่ได้กำหนดซึ่งฉันพบว่าไม่มีเหตุผลในเวลานั้น บทเรียน.
gnasher729

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

@philipxy: มาตรฐานกำหนดสิ่งที่เป็นรูปเป็นร่างและกำหนดไว้อย่างดีและสิ่งที่ไม่ ในกรณีของ Q นี้จะกำหนดพฤติกรรมที่ไม่ได้กำหนดไว้ นอกเหนือจากกฎหมายแล้วยังมีข้อสันนิษฐานที่สมเหตุสมผลว่าคอมไพเลอร์สร้างรหัสที่มีประสิทธิภาพ ใช่คุณพูดถูกมาตรฐานไม่จำเป็นต้องเป็นเช่นนั้น อย่างไรก็ตามมันเป็นข้อสันนิษฐานที่สมเหตุสมผล
Damon

@Damon: มาตรฐานระบุการดำเนินการทั้งหมดที่จำเป็นในการปฏิบัติตามที่กำหนดไว้และการดำเนินการใดที่ไม่จำเป็นต้องปฏิบัติตามที่กำหนดไว้ เนื่องจากงานบางอย่างต้องใช้ความหมายที่หลากหลายกว่างานอื่น ๆ ความล้มเหลวของมาตรฐานในการกำหนดพฤติกรรมของการกระทำบางอย่างไม่ได้หมายความว่าการนำไปใช้งานที่กำหนดไว้นั้นจะไม่เหมาะสมกับงานบางอย่างมากกว่างานที่ไม่มี และความล้มเหลวในการกำหนดพฤติกรรมดังกล่าวจะไม่ทำให้การนำไปใช้ไม่เหมาะสมกับวัตถุประสงค์บางอย่างมากกว่าที่อื่น ๆ ที่กำหนดไว้
supercat

1

มีกฎ :

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

ดังนั้นแม้แต่ x = 100 ก็เป็นผลลัพธ์ที่ใช้ได้

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

แต่นักพัฒนาคอมไพเลอร์สามารถใช้ตรรกะอื่น ๆ ได้


0

ดูเหมือนว่า ++ ฉันจะส่งคืนค่า lvalue แต่ i ++ ส่งกลับค่า rvalue
ดังนั้นรหัสนี้ก็โอเค:

int i = 1;
++i = 10;
cout << i << endl;

อันนี้ไม่ใช่:

int i = 1;
i++ = 10;
cout << i << endl;

สองข้อความข้างต้นสอดคล้องกับ VisualC ++, GCC7.1.1, CLang และ Embarcadero
นั่นคือเหตุผลที่โค้ดของคุณใน VisualC ++ และ GCC7.1.1 คล้ายกับโค้ดต่อไปนี้

int i = 1;
... do something there for instance: ++i; ++i; ...
int x = i + i;

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


ปัญหาเกี่ยวกับ "looks line an lvalue" คือคุณกำลังพูดจากมุมมองของมาตรฐาน C ++ ไม่ใช่คอมไพเลอร์
MSalters

@MSalters คำแถลงนี้สอดคล้องกับ VisualStudio 2019, GCC7.1.1, clang และ Embarcadero และด้วยโค้ดชิ้นแรก ดังนั้นข้อกำหนดจึงสอดคล้องกัน แต่มันทำงานแตกต่างกันสำหรับโค้ดชิ้นที่สอง โค้ดชิ้นที่สองสอดคล้องกับ VisualStudio 2019 และ GCC7.1.1 แต่ไม่สอดคล้องกับ clang และ Embarcadero
armagedescu

3
โค้ดชิ้นแรกในคำตอบของคุณคือ C ++ ที่ถูกกฎหมายดังนั้นจึงเห็นได้ชัดว่าการนำไปใช้นั้นสอดคล้องกับข้อกำหนดเมื่อเทียบกับคำถามแล้ว "do something" ของคุณจะลงท้ายด้วยอัฒภาคทำให้เป็นประโยคเต็ม สิ่งนี้จะสร้างลำดับตามมาตรฐาน C ++ แต่ไม่มีอยู่ในคำถาม
MSalters

@MSalters ฉันต้องการทำเป็นรหัสเทียมที่เทียบเท่ากัน แต่ฉันไม่แน่ใจว่าจะจัดรูปแบบใหม่ได้อย่างไร
armagedescu

0

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

โดยทั่วไป++iเป็นกระบวนการ 2 ขั้นตอนในบริบทนี้:

  1. เพิ่มมูลค่าของ i
  2. อ่านค่าของ i

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

  1. การเพิ่มiสำหรับตัวถูกดำเนินการด้านซ้าย
  2. การเพิ่มขึ้นiสำหรับตัวถูกดำเนินการด้านขวา
  3. อ่านย้อนกลับiสำหรับตัวถูกดำเนินการด้านซ้าย
  4. อ่านย้อนกลับiสำหรับตัวถูกดำเนินการที่ถูกต้อง
  5. ผลรวมสอง: ให้ผล 6

ตอนนี้ที่ผมคิดเกี่ยวกับเรื่องนี้ 6 เหมาะสมที่สุดตามมาตรฐาน สำหรับผลลัพธ์ของ 4 เราจำเป็นต้องมี CPU ซึ่งก่อนอื่นอ่านiอย่างอิสระจากนั้นจึงเพิ่มและเขียนค่ากลับเข้าไปในตำแหน่งเดิม โดยทั่วไปสภาพการแข่งขัน สำหรับค่า 5 เราจำเป็นต้องมีคอมไพเลอร์ที่แนะนำจังหวะ

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


0
#include <stdio.h>


void a1(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = x + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void b2(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = i + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void main(void)
{
    a1();
    // b2();
}

ยินดีต้อนรับสู่ stackoverflow! คุณสามารถระบุข้อ จำกัด สมมติฐานหรือความเข้าใจง่ายในคำตอบของคุณ ดูรายละเอียดเพิ่มเติมเกี่ยวกับวิธีตอบได้ที่ลิงค์นี้: stackoverflow.com/help/how-to-answer
Usama Abdulrehman

0

ขึ้นอยู่กับการออกแบบของคอมไพเลอร์ดังนั้นคำตอบจะขึ้นอยู่กับวิธีที่คอมไพลเลอร์ถอดรหัสคำสั่งการใช้ตัวแปรที่แตกต่างกันสองตัว ++ x และ ++ y แทนเพื่อสร้างตรรกะจะเป็นทางเลือกที่ดีกว่า หมายเหตุ: ouput ขึ้นอยู่กับเวอร์ชันของภาษาเวอร์ชันล่าสุดใน ms visual studio หากมีการอัปเดตดังนั้นหากกฎมีการเปลี่ยนแปลงผลลัพธ์จะออกมา



0

จากลิงค์ลำดับการประเมินผล :

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

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

แต่ถ้าจุดลำดับอยู่ระหว่างนิพจน์ย่อย Exp1 และ Exp2 การคำนวณค่าและผลข้างเคียงของ Exp1 จะเรียงลำดับก่อนการคำนวณค่าทุกครั้งและผลข้างเคียงของ Exp2


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

ความคิดเห็นของคุณไม่ได้กล่าวถึงปัญหาใด ๆ ของฉัน รวมถึงรหัสนั้นไม่ได้กำหนดและไม่ระบุ
philipxy

คำพูดไม่ได้กล่าวถึงไม่ได้กำหนด แต่กล่าวถึงไม่ระบุ ฉันทำเสร็จแล้ว
philipxy

ใช่มันไม่ได้ระบุไว้นั่นเป็นเหตุผลว่าทำไมคอมไพเลอร์ที่แตกต่างกันจึงมีคำสั่งการประเมินที่แตกต่างกัน นั่นเป็นเหตุผลที่คุณจะได้รับเอาต์พุตที่แตกต่างกันสำหรับคอมไพเลอร์ที่แตกต่างกัน
Krishna Kanth Yenumula

ขอให้เรายังคงอภิปรายนี้ในการแชท
Krishna Kanth Yenumula

-4

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

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


1
ฉันเชื่อว่าคุณเข้าใจคำถามผิด คำถามเกี่ยวกับพฤติกรรมทั่วไปหรือพฤติกรรมเฉพาะที่อาจนำไปสู่ผลลัพธ์ที่เฉพาะเจาะจง (ผลลัพธ์ของ x = 4, 5 หรือ 6) หากคุณไม่ชอบการใช้คำว่า "สมเหตุสมผล" ของฉันฉันขอแนะนำคุณไปยังความคิดเห็นของฉันด้านบนซึ่งคุณได้ตอบกลับไป: "การใช้คำว่า" สมเหตุสมผล "เป็นเพียงการกีดกันไม่ให้ใครพูดว่า 'คอมไพเลอร์สามารถทำอะไรก็ได้ แม้กระทั่งปล่อยคำสั่งเดียว '' set x ถึง 7 '' "หากคุณมีถ้อยคำที่ดีกว่าสำหรับคำถามที่ยังคงความคิดทั่วไปฉันก็เปิดรับมัน นอกจากนี้ดูเหมือนว่าคุณโพสต์คำตอบของคุณอีกครั้ง
อบเชย

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