อะไรคือสาเหตุที่ไม่ใช้ [c ++ 17 ของ [nodiscard]] เกือบทุกที่ในรหัสใหม่


70

C ++ 17 แนะนำ[[nodiscard]]คุณสมบัติซึ่งอนุญาตให้โปรแกรมเมอร์ทำเครื่องหมายฟังก์ชั่นในลักษณะที่คอมไพเลอร์สร้างคำเตือนหากวัตถุที่ถูกส่งคืนถูกยกเลิกโดยผู้เรียก สามารถเพิ่มแอตทริบิวต์เดียวกันในประเภทคลาสทั้งหมดได้

ฉันได้อ่านเกี่ยวกับแรงจูงใจของคุณลักษณะนี้ในข้อเสนอเดิมและฉันรู้ว่า C ++ 20 จะเพิ่มคุณสมบัติให้กับฟังก์ชั่นมาตรฐานเช่นstd::vector::emptyชื่อของผู้ที่ไม่ได้สื่อความหมายที่ชัดเจนเกี่ยวกับค่าส่งคืน

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

และไม่ใช่หนึ่งในหลักการออกแบบของ C ++ ที่คอมไพเลอร์ควรตรวจจับข้อผิดพลาดให้ได้มากที่สุด

ถ้าเป็นเช่นนั้นทำไมไม่เพิ่ม[[nodiscard]]รหัสที่ไม่ใช่มรดกของคุณเองลงในvoidฟังก์ชั่นที่ไม่ใช่ฟังก์ชั่นเดียวและเกือบทุกประเภทของคลาส?

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

ตามที่ฉันได้เห็นการอภิปรายเป็นศูนย์เกี่ยวกับความเป็นไปได้นี้ในข้อเสนอมาตรฐานรายการบล็อกคำถามซ้อนมากเกินไปหรือที่อื่นบนอินเทอร์เน็ตฉันต้องหายไปบางสิ่งบางอย่าง

ทำไมกลศาสตร์ดังกล่าวไม่สมเหตุสมผลในรหัส C ++ ใหม่ การใช้คำฟุ่มเฟื่อยเป็นเหตุผลเดียวที่จะไม่ใช้[[nodiscard]]เกือบทุกที่?


[*] ตามทฤษฏีคุณอาจมีลักษณะคล้ายกับ[[maydiscard]]แอตทริบิวต์แทนซึ่งสามารถเพิ่มย้อนหลังให้กับฟังก์ชั่นเช่นprintfในการใช้งานไลบรารีมาตรฐาน


22
มีฟังก์ชั่นมากมายที่สามารถส่งคืนค่าทิ้งได้อย่างสมเหตุสมผล operator =ตัวอย่างเช่น. และstd::map::insert.
user253751

2
@immibis: จริง ห้องสมุดมาตรฐานเต็มไปด้วยฟังก์ชั่นดังกล่าว แต่ในรหัสของฉันพวกเขามักจะหายากมาก คุณคิดว่าเป็นเรื่องของรหัสห้องสมุดกับรหัสแอปพลิเคชันหรือไม่
Christian Hackl

9
"... verbose อย่างมากที่มันเริ่มรู้สึกเหมือน Java ... " - การอ่านสิ่งนี้ในคำถามเกี่ยวกับ C ++ ทำให้ฉันกระตุกนิดหน่อย: - /
Marco13

3
@ChristianHackl ความคิดเห็นอาจไม่ใช่สถานที่ที่เหมาะสมสำหรับ "การทุบตีภาษา" และแน่นอนว่า Java ยังมีความละเอียดมากกว่าภาษาอื่น ๆ แต่ไฟล์ส่วนหัวของผู้ประกอบการที่ได้รับมอบหมาย, การก่อสร้างการคัดลอกและการใช้งานที่เหมาะสมของconstสามารถขยายตัวชั้นเป็นอย่างอื่น "ง่าย" (หรือมากกว่า "ข้อมูลเก่าธรรมดาวัตถุ") อย่างมากใน C ++
Marco13

2
@ Marco13: ฉันไม่สามารถพูดถึงประเด็นมากมายในความคิดเห็นเดียว มาทำให้สั้น: Java ไม่มีไฟล์ส่วนหัวซึ่งลดจำนวนไฟล์ แต่เพิ่มการเชื่อมต่อ OTOH มันบังคับให้คุณใส่คลาสระดับบนสุดในไฟล์ของตัวเองและทุกฟังก์ชั่นในคลาส ใน C ++ คุณแทบไม่เคยใช้ตัวดำเนินการกำหนดค่าและคัดลอก constructors ด้วยตนเอง ฟังก์ชั่นเหล่านั้นมีความเกี่ยวข้องมากขึ้นสำหรับคลาสไลบรารีมาตรฐานเช่นstd::vectorหรือstd::unique_ptrซึ่งคุณต้องการสมาชิกข้อมูลในการกำหนดคลาสของคุณ ฉันทำงานกับทั้งสองภาษา Java เป็นภาษาที่โอเค แต่มันมีความละเอียดมากกว่า
Christian Hackl

คำตอบ:


73

ในรหัสใหม่ที่ไม่จำเป็นต้องใช้งานร่วมกับมาตรฐานที่เก่ากว่าให้ใช้คุณลักษณะนั้นในทุกที่ที่เหมาะสม แต่สำหรับ C ++ [[nodiscard]]ทำให้เป็นค่าเริ่มต้นที่ไม่ดี คุณแนะนำ:

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

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

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

[[nodiscard]]แอตทริบิวต์เป็นประโยชน์มากที่สุดในสองกรณี

  • หากฟังก์ชั่นไม่มีผลกระทบใด ๆ นอกเหนือจากการคืนผลลัพธ์บางอย่างนั่นคือบริสุทธิ์ หากไม่ได้ใช้ผลการโทรจะไม่มีประโยชน์อย่างแน่นอน ในทางกลับกันการทิ้งผลจะไม่ถูกต้อง

  • หากต้องตรวจสอบค่าส่งคืนเช่นอินเทอร์เฟซแบบ C ที่ส่งคืนรหัสข้อผิดพลาดแทนที่จะส่ง นี่เป็นกรณีการใช้งานหลัก สำหรับสำนวน C ++ นั้นจะค่อนข้างหายาก

ทั้งสองกรณีนี้มีช่องว่างขนาดใหญ่ของฟังก์ชันที่ไม่บริสุทธิ์ซึ่งคืนค่าซึ่งการเตือนดังกล่าวอาจทำให้เข้าใจผิด ตัวอย่างเช่น:

  • พิจารณาชนิดข้อมูลคิวด้วย.pop()วิธีที่ลบองค์ประกอบและส่งคืนสำเนาขององค์ประกอบที่ลบออก วิธีดังกล่าวมักจะสะดวก อย่างไรก็ตามมีบางกรณีที่เราต้องการลบองค์ประกอบโดยไม่ได้รับการคัดลอก นั่นถูกต้องตามกฎหมายอย่างสมบูรณ์และคำเตือนจะไม่เป็นประโยชน์ การออกแบบที่แตกต่าง (เช่นstd::vector) แยกความรับผิดชอบเหล่านี้ออก แต่มีการแลกเปลี่ยนอื่น ๆ

    โปรดทราบว่าในบางกรณีจะต้องมีการทำสำเนาอยู่ดีดังนั้นขอบคุณRVO ที่ส่งคืนสำเนาจะให้บริการฟรี

  • พิจารณาส่วนต่อประสานที่คล่องแคล่วซึ่งการดำเนินการแต่ละอย่างจะส่งคืนวัตถุเพื่อให้สามารถดำเนินการต่อไปได้ ใน C ++ <<ตัวอย่างที่พบมากที่สุดคือผู้ประกอบการสตรีมแทรก มันจะยุ่งยากอย่างมากในการเพิ่ม[[nodiscard]]คุณสมบัติให้กับแต่ละ<<เกินพิกัด

ตัวอย่างเหล่านี้แสดงให้เห็นว่าการสร้างรหัส C ++ ที่เป็นสำนวนโดยไม่มีคำเตือนภายใต้ภาษา“ C ++ 17 ที่มี nodiscard โดยค่าเริ่มต้น” จะค่อนข้างน่าเบื่อ

โปรดทราบว่ารหัส C ++ 17 ใหม่ของคุณ (ซึ่งคุณสามารถใช้คุณลักษณะเหล่านี้) อาจยังคงถูกรวบรวมพร้อมกับไลบรารีที่กำหนดเป้าหมายมาตรฐาน C ++ ที่เก่ากว่า ความเข้ากันได้แบบย้อนหลังนี้เป็นสิ่งสำคัญสำหรับระบบนิเวศ C / C ++ ดังนั้นการทำให้ nodiscard เป็นค่าเริ่มต้นจะส่งผลให้มีคำเตือนมากมายสำหรับกรณีการใช้งานทั่วไปที่เป็นสำนวน - คำเตือนที่คุณไม่สามารถแก้ไขได้หากไม่มีการเปลี่ยนแปลงที่มาถึงของซอร์สโค้ดของไลบรารีอย่างกว้างขวาง

เนื้อหาที่นี่ไม่ใช่การเปลี่ยนแปลงของซีแมนทิกส์ แต่คุณสมบัติของมาตรฐาน C ++ แต่ละรายการนั้นใช้กับขอบเขตการรวบรวมต่อหน่วยและไม่ได้อยู่ในขอบเขตต่อไฟล์ หาก / เมื่อบางมาตรฐาน C ++ ในอนาคตย้ายออกจากไฟล์ส่วนหัวการเปลี่ยนแปลงดังกล่าวจะเป็นจริงมากขึ้น


6
ฉันโหวตขึ้นสิ่งนี้เนื่องจากมีข้อมูลเชิงลึกที่เป็นประโยชน์ ยังไม่แน่ใจว่าจะตอบคำถามได้จริงหรือไม่ ฟังก์ชั่นที่ไม่บริสุทธิ์เช่นpopหรือสิ่งoperator<<เหล่านั้นจะถูกทำเครื่องหมายอย่างชัดเจนด้วย[[maydiscard]]คุณสมบัติจินตภาพของฉันดังนั้นจึงไม่มีการสร้างคำเตือน คุณกำลังพูดว่าฟังก์ชั่นดังกล่าวโดยทั่วไปมักจะเป็นเรื่องธรรมดามากกว่าที่ฉันอยากจะเชื่อหรือมากกว่าคนทั่วไปมากกว่าในโค้ดเบสของตัวเอง? ย่อหน้าแรกของคุณเกี่ยวกับรหัสที่มีอยู่นั้นเป็นจริงอย่างแน่นอน แต่ก็ยังทำให้ฉันสงสัยว่าในขณะนี้มีคนอื่นกำลังใช้รหัสใหม่ทั้งหมดของพวกเขาด้วย[[nodiscard]]หรือไม่และทำไมไม่
Christian Hackl

23
@ChristianHackl: มีเวลาในประวัติศาสตร์ของ C ++ ที่มันถือว่าเป็นแนวปฏิบัติที่ดีในการคืนค่าเป็น const คุณไม่สามารถพูดได้returns_an_int() = 0ทำไมต้องreturns_a_str() = ""ทำงาน C ++ 11 แสดงให้เห็นว่านี่เป็นความคิดที่แย่จริง ๆ เพราะตอนนี้รหัสทั้งหมดนี้ห้ามการเคลื่อนไหวเพราะค่าเป็น const ฉันคิดว่าการพาออกจากบทเรียนนั้นคือ "มันไม่ได้ขึ้นอยู่กับสิ่งที่ผู้โทรติดต่อของคุณทำกับผลลัพธ์ของคุณ" การเพิกเฉยต่อผลลัพธ์เป็นสิ่งที่ถูกต้องสมบูรณ์แบบที่ผู้โทรอาจต้องการทำและถ้าคุณรู้ว่ามันผิด (แถบที่สูงมาก) คุณไม่ควรบล็อก
GManNickG

13
คนที่มีสติปัญญาempty()ก็มีเหตุผลเช่นกันเพราะชื่อนั้นค่อนข้างคลุมเครือ: มันเป็นภาคแสดงis_empty()หรือเป็นผู้เปลี่ยนแปลงclear()หรือไม่? โดยทั่วไปแล้วคุณจะไม่คาดหวังว่า mutator นั้นจะส่งคืนสิ่งใดดังนั้นคำเตือนเกี่ยวกับค่าที่ส่งคืนสามารถแจ้งเตือนโปรแกรมเมอร์ถึงปัญหา ด้วยชื่อที่ชัดเจนยิ่งขึ้นแอตทริบิวต์นี้จะไม่จำเป็นเท่าที่ควร
amon

13
@ChristianHackl: ตามที่คนอื่นพูดฉันคิดว่าฟังก์ชั่นที่บริสุทธิ์เป็นตัวอย่างที่ดีของเวลาที่ควรใช้ ฉันจะไปขั้นตอนต่อไปและกล่าวว่าควรจะมีคุณลักษณะและมันควรจะบ่งบอกถึง[[pure]] [[nodiscard]]วิธีนี้ชัดเจนว่าทำไมผลลัพธ์นี้ไม่ควรถูกยกเลิก แต่นอกเหนือจากฟังก์ชั่นบริสุทธิ์ฉันไม่สามารถนึกถึงฟังก์ชั่นอื่น ๆ ที่ควรมีพฤติกรรมนี้ และฟังก์ชั่นส่วนใหญ่ไม่บริสุทธิ์ดังนั้นฉันจึงไม่คิดว่าคนรับใช้โดยปริยายเป็นตัวเลือกที่เหมาะสม
GManNickG

5
@GManNickG: มีความพยายามและล้มเหลวในการแก้ปัญหาที่รหัสละเว้นรหัสข้อผิดพลาดผมขอเชื่อว่าส่วนใหญ่ของรหัสข้อผิดพลาดจะต้องมีการตรวจสอบโดยค่าเริ่มต้น
Mooing Duck

9

เหตุผลที่ฉันจะไม่[[nodiscard]]เกือบทุกที่:

  1. (ที่สำคัญ :) นี้จะแนะนำวิธีที่เสียงมากเกินไปในหัวของฉัน
  2. (หลัก :) ฉันไม่รู้สึกว่าฉันควรตั้งสมมติฐานการเก็งกำไรที่แข็งแกร่งเกี่ยวกับรหัสของคนอื่น คุณต้องการยกเลิกค่าส่งคืนที่ฉันให้หรือไม่ เอาเองออกไป
  3. (เล็กน้อย :) คุณจะรับประกันความเข้ากันไม่ได้กับ C ++ 14

ตอนนี้หากคุณตั้งเป็นค่าเริ่มต้นคุณจะบังคับให้ผู้พัฒนาห้องสมุดทั้งหมดบังคับให้ผู้ใช้ทุกคนไม่ทิ้งค่าส่งคืน นั่นจะน่ากลัว หรือคุณบังคับให้พวกเขาเพิ่ม[[maydiscard]]ฟังก์ชั่นมากมายซึ่งก็น่ากลัวเช่นกัน


0

ดังตัวอย่าง: โอเปอเรเตอร์ << มีค่าส่งคืนที่ขึ้นอยู่กับการโทรที่จำเป็นอย่างยิ่งหรือไร้ประโยชน์อย่างแน่นอน (std :: cout << x << y จำเป็นต้องมีอันแรกเพราะมันจะส่งคืนกระแสที่สองไม่ได้ใช้เลย) ตอนนี้เปรียบเทียบกับ printf ที่ทุกคนทิ้งค่าส่งคืนซึ่งมีไว้สำหรับการตรวจสอบข้อผิดพลาด แต่จากนั้นโอเปอเรเตอร์ << ก็ไม่มีการตรวจสอบข้อผิดพลาดดังนั้นจึงไม่มีประโยชน์ในตอนแรก ดังนั้นในทั้งสองกรณีก็แค่ปมที่จะสร้างความเสียหาย


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