อะไรที่ทำให้ i = i ++ + 1; ถูกกฎหมายใน C ++ 17?


186

ก่อนที่คุณจะเริ่มตะโกนพฤติกรรมที่ไม่ได้กำหนดสิ่งนี้จะปรากฏในN4659 (C ++ 17) อย่างชัดเจน

  i = i++ + 1;        // the value of i is incremented

ยังอยู่ในN3337 (C ++ 11)

  i = i++ + 1;        // the behavior is undefined

มีอะไรเปลี่ยนแปลง

จากสิ่งที่ฉันสามารถรวบรวมได้จาก[N4659 basic.exec]

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

ตำแหน่งที่กำหนดค่าไว้ที่[N4659 basic.type]

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

จาก[N3337 basic.exec]

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

เช่นเดียวกันค่าจะถูกกำหนดที่[N3337 basic.type]

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

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

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

ซึ่งไม่ส่งผลกระทบต่อตัวอย่าง

จาก[N4659 expr.ass]

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

จาก[N3337 expr.ass]

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

ความแตกต่างเพียงอย่างเดียวคือประโยคสุดท้ายที่หายไปใน N3337

ประโยคสุดท้ายไม่ควรมีความสำคัญใด ๆ เนื่องจากตัวถูกดำเนินการด้านซ้ายiไม่ใช่"ผลข้างเคียงอื่น"หรือ"การใช้ค่าของวัตถุสเกลาร์เดียวกัน"เนื่องจากการแสดงออกของidเป็นค่า lvalue


23
คุณระบุสาเหตุที่: ใน C ++ 17 ตัวถูกดำเนินการด้านขวาจะถูกจัดลำดับก่อนตัวถูกดำเนินการด้านซ้าย ใน C ++ 11 ไม่มีการจัดลำดับดังกล่าว คำถามของคุณคืออะไร?
Robᵩ

4
@ Robᵩดูประโยคสุดท้าย
สัญจรโดย

7
ใครบ้างมีลิงก์ไปยังแรงจูงใจสำหรับการเปลี่ยนแปลงนี้ ผมอยากจะวิเคราะห์คงที่จะสามารถที่จะพูดว่า "คุณไม่ต้องการที่จะทำอย่างนั้น" i = i++ + 1;เมื่อต้องเผชิญกับรหัสเช่น

7
@ NeilButterworth มาจากกระดาษp0145r3.pdf : "การกลั่นลำดับการประเมินผลนิพจน์สำหรับ Idiomatic C ++"
xaizek

9
@ NeilButterworth, ส่วนที่ 2 บอกว่านี่เป็นเรื่องง่ายและแม้กระทั่งผู้เชี่ยวชาญก็ไม่สามารถทำสิ่งที่ถูกต้องในทุกกรณี นั่นเป็นแรงจูงใจทั้งหมดของพวกเขา
xaizek

คำตอบ:


144

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

ความหวังเดียวในกรณีนี้คือการรับประกันเพิ่มเติมใด ๆ ที่ดำเนินการโดยผู้ให้บริการเฉพาะที่ใช้ใน RHS หาก RHS ใช้คำนำหน้า++คุณสมบัติการเรียงลำดับที่เฉพาะเจาะจงกับรูปแบบคำนำหน้าของ++จะได้รับการบันทึกวันในตัวอย่างนี้ แต่ postfix ++เป็นเรื่องที่แตกต่าง: มันไม่ได้รับประกันเช่นนั้น ใน C ++ 11 ผลข้างเคียงของ=และ postfix จะ++ไม่ปรากฏตามมาซึ่งสัมพันธ์กันในตัวอย่างนี้ และนั่นคือ UB

ใน C ++ 17 จะมีการเพิ่มประโยคพิเศษลงในข้อมูลจำเพาะของโอเปอเรเตอร์การมอบหมาย:

ตัวถูกดำเนินการด้านขวาถูกจัดลำดับก่อนหน้าตัวถูกดำเนินการด้านซ้าย

เมื่อรวมกับด้านบนจะทำให้การรับประกันแข็งแกร่งมาก มันเรียงลำดับทุกสิ่งที่เกิดขึ้นใน RHS (รวมถึงผลข้างเคียงใด ๆ ) ก่อนทุกอย่างที่เกิดขึ้นใน LHS เนื่องจากการมอบหมายจริงถูกจัดลำดับหลังจาก LHS (และ RHS) ลำดับพิเศษนั้นแยกการกระทำของการมอบหมายจากผลข้างเคียงใด ๆ ที่มีอยู่ใน RHS อย่างสมบูรณ์ การจัดลำดับที่แข็งแกร่งกว่านี้คือสิ่งที่ช่วยกำจัด UB ด้านบน

(อัปเดตเพื่อพิจารณาความคิดเห็น @John Bollinger)


3
มันถูกต้องหรือไม่ที่จะรวม "การกระทำตามที่ได้รับมอบหมายจริง ๆ " ในเอฟเฟกต์ที่ครอบคลุมโดย "ตัวถูกดำเนินการมือซ้าย" ในข้อความที่ตัดตอนมา? มาตรฐานมีภาษาแยกต่างหากเกี่ยวกับการจัดลำดับของการมอบหมายจริง ฉันใช้ข้อความที่ตัดตอนมาที่คุณเสนอให้ถูก จำกัด ในขอบเขตของการเรียงลำดับของนิพจน์ย่อยทางซ้ายและขวาซึ่งดูเหมือนจะไม่เพียงพอเมื่อรวมกับส่วนที่เหลือของส่วนนั้นเพื่อสนับสนุนอย่างดี คำจำกัดความของคำสั่ง OP
John Bollinger

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

3
@JohnBollinger: ฉันคิดว่ามันแปลกที่ผู้เขียนมาตรฐานจะทำการเปลี่ยนแปลงที่บั่นทอนประสิทธิภาพของการสร้างรหัสที่ตรงไปตรงมาและไม่เคยมีความจำเป็นในอดีตและยังขัดขวางการกำหนดพฤติกรรมอื่น ๆ ที่ไม่มีปัญหาที่ใหญ่กว่ามาก จะไม่ค่อยก่อให้เกิดอุปสรรคใด ๆ ที่มีความหมายต่อประสิทธิภาพ
supercat

1
@Kaz: สำหรับการมอบหมายงานสารประกอบดำเนินการประเมินความคุ้มค่าทางด้านซ้ายหลังด้านขวาช่วยให้สิ่งที่ต้องการx -= y;ที่จะได้รับการประมวลผลมากกว่าmov eax,[y] / sub [x],eax mov eax,[x] / neg eax / add eax,[y] / mov [x],eaxฉันไม่เห็นสิ่งที่เกี่ยวกับไอดอลแบบนั้น หากต้องระบุการเรียงลำดับการจัดเรียงที่มีประสิทธิภาพมากที่สุดน่าจะเป็นการคำนวณทั้งหมดที่จำเป็นในการระบุวัตถุด้านซ้ายก่อนจากนั้นประเมินค่าตัวถูกดำเนินการที่เหมาะสมจากนั้นประเมินค่าของวัตถุด้านซ้าย แต่ต้องมีคำ สำหรับการดำเนินการแก้ไข id'ty ของวัตถุด้านซ้าย
supercat

1
@Kaz: ถ้าxและyถูกvolatileนั่นจะมีผลข้างเคียง นอกจากนี้การพิจารณาเดียวกันจะนำไปใช้กับx += f();ที่ปรับเปลี่ยนf() x
supercat

33

คุณระบุประโยคใหม่

ตัวถูกดำเนินการด้านขวาถูกจัดลำดับก่อนหน้าตัวถูกดำเนินการด้านซ้าย

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


7

ในมาตรฐาน C ++ ที่เก่ากว่าและใน C11 คำจำกัดความของข้อความโอเปอเรเตอร์การมอบหมายจะลงท้ายด้วยข้อความ:

การประเมินผลของตัวถูกดำเนินการจะไม่เกิดขึ้นตามมา

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

ข้อความนี้ถูกลบเพียงแค่ใน C ++ 11 ทำให้ค่อนข้างคลุมเครือ มันเป็น UB หรือไม่? สิ่งนี้ได้รับการอธิบายอย่างชัดเจนใน C ++ 17 ที่เพิ่ม:

ตัวถูกดำเนินการด้านขวาถูกจัดลำดับก่อนหน้าตัวถูกดำเนินการด้านซ้าย


ในฐานะที่เป็นบันทึกด้านข้างในมาตรฐานที่เก่ากว่าทั้งหมดนี้ได้ทำให้ชัดเจนมากตัวอย่างจาก C99:

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

โดยทั่วไปใน C11 / C ++ 11 พวกเขาสับสนเมื่อพวกเขาลบข้อความนี้


1

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

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

i = i++;

นี่+ 1คือปลาเฮอริ่งแดงและก็ไม่ชัดเจนว่าทำไมมาตรฐานใช้มันในตัวอย่างของพวกเขาแม้ว่าฉันจะจำคนที่โต้เถียงในรายชื่อผู้รับจดหมายก่อน C ++ 11 ที่อาจจะ+ 1สร้างความแตกต่างเนื่องจากการบังคับให้แปลง lvalue ก่อนด้านขวา - ด้านซ้ายมือ. แน่นอนว่าไม่มีสิ่งใดใน C ++ 17 (และอาจไม่เคยใช้กับ C ++ ทุกรุ่น)

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