คำตอบที่ถูกต้องสำหรับ cout << a ++ << a ;?


98

เมื่อเร็ว ๆ นี้ในการสัมภาษณ์มีคำถามประเภทวัตถุประสงค์ดังต่อไปนี้

int a = 0;
cout << a++ << a;

คำตอบ:

ก. 10
ข. 01
ค. พฤติกรรมที่ไม่ได้กำหนด

ฉันตอบตัวเลือก b คือเอาต์พุตจะเป็น "01"

แต่ฉันต้องประหลาดใจในภายหลังฉันได้รับแจ้งจากผู้สัมภาษณ์ว่าคำตอบที่ถูกต้องคือตัวเลือก c: ไม่ได้กำหนด

ตอนนี้ฉันรู้แนวคิดของลำดับจุดใน C ++ แล้ว พฤติกรรมไม่ได้กำหนดไว้สำหรับคำสั่งต่อไปนี้:

int i = 0;
i += i++ + i++;

แต่ตามความเข้าใจของผมสำหรับคำสั่งcout << a++ << aที่ostream.operator<<()จะเรียกว่าเป็นครั้งที่สองครั้งแรกกับและต่อมาostream.operator<<(a++)ostream.operator<<(a)

ฉันยังตรวจสอบผลลัพธ์บนคอมไพเลอร์ VS2010 และเอาต์พุตเป็น '01' ด้วย


30
คุณไม่ขอคำอธิบาย? ฉันมักจะสัมภาษณ์ผู้มีโอกาสเป็นผู้สมัครและค่อนข้างสนใจที่จะรับคำถามมันแสดงความสนใจ
Brady

3
@jrok เป็นพฤติกรรมที่ไม่ได้กำหนด ทุกสิ่งที่นำไปใช้ (รวมถึงการส่งอีเมลดูถูกในชื่อของคุณไปยังหัวหน้าของคุณ) เป็นไปตามข้อกำหนด
James Kanze

2
คำถามนี้กำลังเรียกหาคำตอบ C ++ 11 ( เวอร์ชันปัจจุบันของ C ++) ที่ไม่ได้กล่าวถึงลำดับจุด น่าเสียดายที่ฉันมีความรู้ไม่เพียงพอเกี่ยวกับการแทนที่จุดลำดับใน C ++ 11
CB Bailey

3
ถ้ามันไม่ได้ไม่ได้กำหนดแน่นอนมันไม่อาจจะ10มันจะเป็นอย่างใดอย่างหนึ่งหรือ01 00( c++มักจะประเมินค่าcได้ก่อนที่จะถูกเพิ่มขึ้น) และแม้ว่าจะไม่ได้กำหนดไว้ แต่ก็ยังคงสับสนอย่างมาก
leftaround ประมาณ

2
รู้ไหมเมื่อฉันอ่านชื่อเรื่อง“ cout << c ++ << c” ฉันคิดว่ามันเป็นข้อความเกี่ยวกับความสัมพันธ์ระหว่างภาษา C และ C ++ ในขณะนั้นและอีกภาษาหนึ่งชื่อ“ cout” คุณรู้ไหมว่าเหมือนมีคนพูดว่าพวกเขาคิดว่า“ cout” นั้นด้อยกว่า C ++ มากอย่างไรและ C ++ นั้นด้อยกว่า C มากและอาจเป็นเพราะการเปลี่ยนแปลงที่“ cout” นั้นด้อยกว่า C มาก :)
tchrist

คำตอบ:


145

คุณสามารถนึกถึง:

cout << a++ << a;

เช่น:

std::operator<<(std::operator<<(std::cout, a++), a);

c ++ รับประกันว่าผลข้างเคียงทั้งหมดของการประเมินก่อนหน้านี้จะได้รับการดำเนินการในจุดลำดับ ไม่มีจุดลำดับระหว่างการประเมินอาร์กิวเมนต์ของฟังก์ชันซึ่งหมายความว่าอาร์กิวเมนต์aสามารถประเมินได้ก่อนอาร์กิวเมนต์std::operator<<(std::cout, a++)หรือหลัง ดังนั้นผลลัพธ์ข้างต้นจึงไม่ได้กำหนดไว้


ปรับปรุง C ++ 17

ใน C ++ 17 กฎได้รับการอัปเดต โดยเฉพาะอย่างยิ่ง:

ในการแสดงออกของผู้ประกอบการเปลี่ยนE1<<E2และE1>>E2ทุกมูลค่าการคำนวณและผลข้างเคียงของการเป็นลำดับขั้นตอนทุกครั้งก่อนการคำนวณมูลค่าและผลข้างเคียงของE1E2

ซึ่งหมายความว่ามันต้องใช้รหัสเพื่อผลการผลิตซึ่งเอาท์พุทb01

ดูP0145R3 ลำดับการประเมินนิพจน์การกลั่นสำหรับ Idiomatic C ++สำหรับรายละเอียดเพิ่มเติม


@ Maxim: ขอบคุณสำหรับการอธิบาย เมื่อคุณโทรออกมันจะเป็นพฤติกรรมที่ไม่ได้กำหนด แต่ตอนนี้ฉันมีอีกหนึ่งคำถาม (อาจจะเป็นซิลเลอร์และฉันพลาดบางอย่างพื้นฐานและคิดดัง ๆ ) คุณสรุปได้อย่างไรว่าเวอร์ชันสากลของ std :: operator << () จะถูกเรียกแทน ostream :: operator < <() รุ่นสมาชิก ในการดีบักฉันเชื่อมโยงไปถึงในเวอร์ชันสมาชิกของการโทร ostream :: operator << () แทนที่จะเป็นเวอร์ชันสากลและนั่นคือเหตุผลที่ตอนแรกฉันคิดว่าคำตอบนั้นน่าจะเป็น 01
pravs

@Maxim ไม่ว่ามันจะทำให้แตกต่างกัน แต่เนื่องจากcมีประเภทintที่operator<<นี่มีฟังก์ชั่นสมาชิก
James Kanze

2
@pravs: ไม่ว่าoperator<<จะเป็นฟังก์ชันสมาชิกหรือฟังก์ชันยืนอิสระจะไม่ส่งผลต่อจุดลำดับ
Maxim Egorushkin

11
'จุดลำดับ' ไม่ได้ใช้ในมาตรฐาน C ++ อีกต่อไป ไม่ชัดเจนและถูกแทนที่ด้วยความสัมพันธ์ 'ลำดับก่อน / ลำดับหลัง'
Rafał Dowgird

2
So the result of the above is undefined.คำอธิบายของคุณเป็นเพียงที่ดีสำหรับการที่ไม่ได้ระบุไม่ได้สำหรับการที่ไม่ได้กำหนด JamesKanze อธิบายว่ามันมากขึ้นสาปแช่งไม่ได้กำหนด ในคำตอบของเขาแม้ว่า
Deduplicator

68

เทคนิคทั้งหมดนี้เป็นพฤติกรรมที่ไม่ได้กำหนด

แต่มีสองประเด็นสำคัญสำหรับคำตอบ

คำสั่งรหัส:

std::cout << a++ << a;

ได้รับการประเมินเป็น:

std::operator<<(std::operator<<(std::cout, a++), a);

มาตรฐานไม่ได้กำหนดลำดับของการประเมินอาร์กิวเมนต์ให้กับฟังก์ชัน
ดังนั้น:

  • std::operator<<(std::cout, a++) ได้รับการประเมินก่อนหรือ
  • aได้รับการประเมินก่อนหรือ
  • อาจเป็นคำสั่งที่กำหนดไว้สำหรับการนำไปใช้งาน

คำสั่งซื้อนี้ไม่ระบุ[Ref 1]ตามมาตรฐาน

[Ref 1] C ++ 03 5.2.2 การเรียกใช้ฟังก์ชัน
ย่อหน้าที่ 8

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

นอกจากนี้ไม่มีจุดลำดับระหว่างการประเมินผลของการขัดแย้งกับฟังก์ชัน แต่จุดลำดับที่มีอยู่เฉพาะหลังจากการประเมินผลของการขัดแย้งทั้งหมด[Ref 2]

[Ref 2] C ++ 03 1.9 การเรียกใช้โปรแกรม [intro.execution]:
ย่อหน้าที่ 17:

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

โปรดทราบว่านี่คือค่าของcการเข้าถึงมากกว่าหนึ่งครั้งโดยไม่มีจุดลำดับการแทรกแซงเกี่ยวกับเรื่องนี้มาตรฐานกล่าวว่า:

[Ref 3] C ++ 03 5 นิพจน์ [expr]:
ย่อหน้าที่ 4:

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

รหัสแก้ไขcมากกว่าหนึ่งครั้งโดยไม่มีการแทรกแซงจุดลำดับและไม่มีการเข้าถึงเพื่อกำหนดค่าของวัตถุที่จัดเก็บ นี่คือการละเมิดที่ชัดเจนของข้อข้างต้นและด้วยเหตุนี้ผลที่ได้รับคำสั่งจากมาตรฐานที่มีพฤติกรรมที่ไม่ได้กำหนด[Ref 3]


1
ในทางเทคนิคแล้วพฤติกรรมนั้นไม่ได้กำหนดไว้เนื่องจากมีการดัดแปลงวัตถุและเข้าถึงได้จากที่อื่นโดยไม่มีจุดลำดับที่แทรกแซง ไม่ระบุไม่ระบุ; ทำให้การนำไปใช้งานมีความคล่องตัวมากยิ่งขึ้น
James Kanze

1
@ แอลใช่ ฉันไม่เห็นการแก้ไขของคุณ (แม้ว่าฉันจะตอบสนองต่อคำสั่งของ jrok ว่าโปรแกรมไม่สามารถทำอะไรแปลก ๆ ได้ - ทำได้) รุ่นที่แก้ไขของคุณเป็นสิ่งที่ดีเท่าที่มันจะไป แต่ในใจของฉันคำที่สำคัญคือการสั่งซื้อบางส่วน ; ลำดับจุดแนะนำเฉพาะการจัดลำดับบางส่วน
James Kanze

1
@Als ขอบคุณสำหรับคำอธิบายอย่างละเอียดมีประโยชน์มากจริงๆ !!
pravs

4
มาตรฐาน C ++ 0x ใหม่กล่าวว่าโดยพื้นฐานแล้วเหมือนกัน แต่อยู่ในส่วนที่แตกต่างกันและใช้ถ้อยคำที่แตกต่างกัน :) คำพูด: (1.9 Program Execution [intro.execution], พาร์ 15): "หากผลข้างเคียงที่มีต่อวัตถุสเกลาร์จะไม่สัมพันธ์กับ ไม่ว่าจะเป็นผลข้างเคียงอื่นที่มีต่อวัตถุสเกลาร์เดียวกันหรือการคำนวณค่าโดยใช้ค่าของวัตถุสเกลาร์เดียวกันพฤติกรรมจะไม่ถูกต้อง "
Rafał Dowgird

2
ฉันเชื่อว่ามีข้อบกพร่องในคำตอบนี้ "std :: cout << c ++ << c;" ไม่สามารถแปลเป็น "std :: operator << (std :: operator << (std :: cout, c ++), c)" เนื่องจาก std :: operator << (std :: ostream &, int) ไม่มีอยู่ แต่จะแปลเป็น "std :: cout.operator << (c ++). operator (c);" ซึ่งจริงๆแล้วจะมีจุดลำดับระหว่างการประเมิน "c ++" และ "c" (ตัวดำเนินการที่โอเวอร์โหลดถือเป็น การเรียกใช้ฟังก์ชันจึงมีจุดลำดับเมื่อการเรียกฟังก์ชันกลับมา) ดังนั้นพฤติกรรมและการดำเนินการเพื่อที่จะระบุ
Christopher Smith

20

ลำดับจุดกำหนดลำดับบางส่วนเท่านั้น ในกรณีของคุณคุณมี (เมื่อแก้ไขปัญหาเกินเสร็จแล้ว):

std::cout.operator<<( a++ ).operator<<( a );

มีจุดลำดับระหว่างเป็นa++และสายแรกที่ std::ostream::operator<<และมีจุดลำดับระหว่างสองaและสายที่สองไปstd::ostream::operator<<แต่ไม่มีจุดลำดับระหว่างa++และa; ข้อ จำกัด เพียงการสั่งซื้อที่a++ได้รับการประเมินอย่างเต็มที่ (รวมถึงผลข้างเคียง) ก่อนที่จะสายแรกที่operator<<และที่สองได้รับการประเมินอย่างเต็มที่ก่อนที่จะสายที่สองไปa operator<<(นอกจากนี้ยังมีข้อ จำกัด ในการสั่งซื้อเชิงสาเหตุ: การเรียกครั้งที่สองไปoperator<<ยังไม่สามารถนำหน้าครั้งแรกได้เนื่องจากต้องใช้ผลลัพธ์ของครั้งแรกเป็นอาร์กิวเมนต์) §5 / 4 (C ++ 03) สถานะ:

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

หนึ่งใน orderings อนุญาตในการแสดงออกของคุณa++, a, สายแรกที่operator<<โทรไปสองoperator<<; สิ่งนี้จะแก้ไขค่าที่เก็บไว้ของa( a++) และเข้าถึงค่าอื่นที่ไม่ใช่เพื่อกำหนดค่าใหม่ (ค่าที่สองa) พฤติกรรมไม่ได้กำหนดไว้


สิ่งที่จับได้จากคำพูดของคุณเกี่ยวกับมาตรฐาน "ยกเว้นที่ระบุไว้" IIRC มีข้อยกเว้นเมื่อจัดการกับตัวดำเนินการที่โอเวอร์โหลดซึ่งถือว่าตัวดำเนินการเป็นฟังก์ชันดังนั้นจึงสร้างจุดลำดับระหว่างการเรียกครั้งแรกและครั้งที่สองไปยังตัวดำเนินการ std :: ostream :: << (int ). กรุณาแก้ไขฉันถ้าฉันผิด
Christopher Smith

@ChristopherSmith ตัวดำเนินการที่ทำงานหนักเกินไปจะทำงานเหมือนเรียกฟังก์ชัน หากcถูกประเภทของผู้ใช้กับผู้ใช้กำหนด++แทนintผลที่จะได้รับการระบุ แต่จะไม่มีพฤติกรรมที่ไม่ได้กำหนด
James Kanze

1
@ChristopherSmith คุณเห็นจุดลำดับระหว่างทั้งสองcในfoo(foo(bar(c)), c)ที่ไหน? มีจุดลำดับเมื่อฟังก์ชั่นนี้จะเรียกว่าเป็นและเมื่อพวกเขากลับมา cแต่ไม่มีการเรียกใช้ฟังก์ชันที่จำเป็นระหว่างการประเมินผลของทั้งสอง
James Kanze

1
@ChristopherSmith ถ้าcเป็น UDT ตัวดำเนินการที่โอเวอร์โหลดจะเป็นการเรียกใช้ฟังก์ชันและจะแนะนำจุดลำดับดังนั้นพฤติกรรมจะไม่ถูกกำหนด แต่จะยังไม่สามารถระบุได้ว่านิพจน์ย่อยcได้รับการประเมินก่อนหรือหลังc++ดังนั้นไม่ว่าคุณจะได้รับเวอร์ชันที่เพิ่มขึ้นหรือไม่ก็ตามจะไม่มีการระบุ (และในทางทฤษฎีไม่จำเป็นต้องเหมือนกันทุกครั้ง)
James Kanze

1
@ChristopherSmith ทุกอย่างก่อนจุดลำดับจะเกิดขึ้นก่อนอะไรหลังจุดลำดับ แต่จุดลำดับจะกำหนดลำดับบางส่วนเท่านั้น ในนิพจน์ที่เป็นปัญหาตัวอย่างเช่นไม่มีจุดลำดับระหว่างนิพจน์ย่อยcและc++ทั้งสองจึงอาจเกิดขึ้นในลำดับใดก็ได้ สำหรับอัฒภาค ... พวกมันทำให้เกิดจุดลำดับเท่าที่จะเป็นนิพจน์เต็มเท่านั้น จุดลำดับที่สำคัญอื่น ๆ คือการเรียกใช้ฟังก์ชัน:f(c++)จะเห็นเพิ่มขึ้นcในfและผู้ประกอบการจุลภาค, &&, ||และ?:ยังเป็นสาเหตุจุดลำดับ
James Kanze

4

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


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