คำถามของคุณยืนยันว่า "การเขียนรหัสที่ปลอดภัยเป็นเรื่องยากมาก" ฉันจะตอบคำถามของคุณก่อนแล้วตอบคำถามที่ซ่อนอยู่ข้างหลังพวกเขา
ตอบคำถาม
คุณเขียนโค้ดที่ปลอดภัยยกเว้นจริงๆหรือ
แน่นอนฉันทำ.
นี่คือเหตุผล Java หายไปจำนวนมากของการอุทธรณ์ของมันให้ฉันเป็น C ++ Programmer (ขาดความหมาย RAII) แต่ฉันกำลัง digressing: นี่คือ C ++ คำถาม
ในความเป็นจริงมันจำเป็นเมื่อคุณจำเป็นต้องทำงานกับรหัส STL หรือ Boost ตัวอย่างเช่นเธรด C ++ ( boost::thread
หรือstd::thread
) จะส่งข้อยกเว้นเพื่อออกจากระบบอย่างสุภาพ
คุณแน่ใจหรือว่ารหัส "พร้อมใช้การผลิตล่าสุด" นั้นปลอดภัยยกเว้น?
คุณแน่ใจได้มั้ยว่ามันคืออะไร?
การเขียนโค้ดที่ปลอดภัยยกเว้นนั้นเหมือนกับการเขียนโค้ดที่ไม่มีข้อบกพร่อง
คุณไม่สามารถมั่นใจได้ 100% ว่ารหัสของคุณปลอดภัยยกเว้น แต่จากนั้นคุณก็พยายามทำโดยใช้รูปแบบที่รู้จักกันดีและหลีกเลี่ยงรูปแบบต่อต้านที่รู้จักกันดี
คุณรู้จักและ / หรือใช้ทางเลือกที่ใช้ได้จริงหรือไม่?
มีไม่มีทางเลือกปฏิบัติใน C ++ (เช่นคุณจะต้องกลับไปที่ C และหลีกเลี่ยงการ c ++ ห้องสมุดเช่นเดียวกับความผิดภายนอกเช่น Windows SEH)
การเขียนรหัสที่ปลอดภัยยกเว้น
ในการเขียนโค้ดที่ปลอดภัยยกเว้นคุณต้องรู้ก่อนว่าระดับความปลอดภัยของข้อยกเว้นแต่ละคำสั่งที่คุณเขียนคืออะไร
ตัวอย่างเช่น a new
สามารถโยนข้อยกเว้นได้ แต่การกำหนดบิวด์อิน (เช่น int หรือตัวชี้) จะไม่ล้มเหลว การสลับจะไม่ล้มเหลว (ไม่เคยเขียนการสลับการโยน), การstd::list::push_back
โยนสามารถ ...
รับประกันข้อยกเว้น
สิ่งแรกที่ต้องทำความเข้าใจคือคุณต้องสามารถประเมินการรับประกันข้อยกเว้นที่นำเสนอโดยฟังก์ชันทั้งหมดของคุณ:
- ไม่มี : รหัสของคุณไม่ควรเสนอ รหัสนี้จะรั่วไหลทุกอย่างและแยกแยะที่ข้อยกเว้นแรกที่ถูกโยน
- พื้นฐาน : นี่คือการรับประกันที่คุณต้องมีอย่างน้อยที่สุดข้อเสนอนั่นคือถ้ามีข้อยกเว้นถูกโยนไม่มีทรัพยากรรั่วไหลออกมาและวัตถุทั้งหมดยังคงเป็นทั้งหมด
- strong : การประมวลผลจะสำเร็จหรือส่งข้อยกเว้น แต่ถ้าส่งออกข้อมูลจะอยู่ในสถานะเดียวกับถ้าการประมวลผลไม่ได้เริ่มเลย (ซึ่งจะให้พลังงานการทำธุรกรรมไปที่ C ++)
- nothrow / nofail : การประมวลผลจะประสบความสำเร็จ
ตัวอย่างของรหัส
รหัสต่อไปนี้ดูเหมือนว่าถูกต้อง C ++ แต่โดยความจริงแล้วเป็นการรับประกันว่า "ไม่มี" และดังนั้นจึงไม่ถูกต้อง:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
ฉันเขียนโค้ดทั้งหมดโดยคำนึงถึงการวิเคราะห์เช่นนี้
การรับประกันต่ำสุดที่เสนอนั้นเป็นพื้นฐาน แต่จากนั้นการเรียงลำดับของแต่ละคำสั่งจะทำให้ฟังก์ชั่นทั้งหมด "ไม่มี" เพราะถ้า 3. พ่น, x จะรั่วไหล
สิ่งแรกที่ต้องทำคือการทำให้ฟังก์ชั่น "พื้นฐาน" นั้นคือการใส่ x ในตัวชี้สมาร์ทจนกว่ามันจะเป็นเจ้าของรายการอย่างปลอดภัย:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
ตอนนี้รหัสของเราให้การรับประกัน "พื้นฐาน" ไม่มีอะไรจะรั่วไหลและวัตถุทั้งหมดจะอยู่ในสถานะที่ถูกต้อง แต่เราสามารถเสนอได้มากกว่านั่นคือการรับประกันที่แข็งแกร่ง นี่คือสิ่งที่มันจะกลายเป็นค่าใช้จ่ายและนี่คือเหตุผลว่าทำไมรหัส C ++ ทั้งหมดไม่แข็งแรง มาลองดูกัน:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
เราสั่งซื้อการดำเนินการอีกครั้งสร้างและตั้งค่าX
ให้ถูกต้องก่อน หากการดำเนินการใด ๆ ล้มเหลวt
จะไม่มีการแก้ไขดังนั้นการดำเนินการ 1 ถึง 3 อาจถือได้ว่า "แข็งแรง": หากมีบางอย่างผิดปกติt
จะไม่ได้รับการแก้ไขและX
จะไม่รั่วไหลเนื่องจากเป็นตัวชี้สมาร์ท
จากนั้นเราจะสร้างสำเนาt2
ของt
และทำงานกับสำเนานี้จากการดำเนินการ 4 ถึง 7 หากมีบางสิ่งบางอย่างผิดเพี้ยนt2
มีการแก้ไข แต่จากนั้นt
ยังคงเป็นต้นฉบับ เรายังคงเสนอการรับประกันที่แข็งแกร่ง
จากนั้นเราจะแลกเปลี่ยนและt
t2
การดำเนินการสลับควรจะไม่ได้รับการแปลใน C ++ ดังนั้นหวังว่าการสลับที่คุณเขียนT
นั้นจะไม่เกิดขึ้น (หากไม่ใช่ให้ลองเขียนใหม่เพื่อไม่เป็นการหยุดทำงาน)
ดังนั้นหากเราไปถึงจุดสิ้นสุดของฟังก์ชั่นทุกอย่างสำเร็จ (ไม่จำเป็นต้องมีชนิดส่งคืน) และt
มีค่าที่ยกเว้น ถ้ามันล้มเหลวก็t
ยังคงมีค่าเดิม
ตอนนี้การเสนอการรับประกันที่แข็งแกร่งอาจมีค่าใช้จ่ายสูงดังนั้นอย่าพยายามเสนอการรับประกันที่แข็งแกร่งให้กับรหัสทั้งหมดของคุณ แต่ถ้าคุณสามารถทำได้โดยไม่เสียค่าใช้จ่าย (และการฝัง C ++ และการเพิ่มประสิทธิภาพอื่น ๆ จากนั้นทำ ผู้ใช้ฟังก์ชั่นจะขอบคุณสำหรับมัน
ข้อสรุป
มันใช้นิสัยในการเขียนรหัสยกเว้นที่ปลอดภัย คุณจะต้องประเมินการรับประกันที่เสนอโดยแต่ละคำสั่งที่คุณใช้แล้วคุณจะต้องประเมินการรับประกันที่เสนอโดยรายการคำแนะนำ
แน่นอนว่าคอมไพเลอร์ C ++ จะไม่สำรองการรับประกัน (ในรหัสของฉันฉันมีการรับประกันเป็นแท็ก @warning doxygen) ซึ่งค่อนข้างเศร้า แต่ก็ไม่ควรหยุดคุณจากการพยายามเขียนโค้ดที่ปลอดภัย
ความล้มเหลวปกติเทียบกับข้อผิดพลาด
โปรแกรมเมอร์สามารถรับประกันได้อย่างไรว่าฟังก์ชั่นไม่ล้มเหลวจะประสบความสำเร็จได้ตลอดเวลา? ท้ายที่สุดแล้วฟังก์ชั่นอาจมีข้อผิดพลาด
นี่เป็นเรื่องจริง การรับประกันข้อยกเว้นควรได้รับการเสนอโดยรหัสที่ปราศจากข้อบกพร่อง แต่ในภาษาใด ๆ การเรียกใช้ฟังก์ชันจะถือว่าฟังก์ชันนั้นไม่มีข้อบกพร่อง ไม่มีรหัสสติป้องกันตัวเองจากความเป็นไปได้ของมันมีข้อผิดพลาด เขียนรหัสที่ดีที่สุดที่คุณสามารถทำได้จากนั้นเสนอการรับประกันโดยไม่มีข้อผิดพลาด และหากมีข้อผิดพลาดแก้ไขให้ถูกต้อง
ข้อยกเว้นสำหรับความล้มเหลวในการประมวลผลที่ยอดเยี่ยมไม่ใช่สำหรับข้อบกพร่องของรหัส
คำสุดท้าย
ตอนนี้คำถามคือ "นี่มันคุ้มค่าหรือไม่"
แน่นอนมันเป็น มีฟังก์ชั่น "nothrow / no-Fail" การรู้ว่าฟังก์ชั่นจะไม่ล้มเหลวเป็นประโยชน์อย่างมาก ฟังก์ชันเดียวกันสามารถพูดได้ว่าเป็น "strong" ซึ่งช่วยให้คุณสามารถเขียนโค้ดที่มีความหมายของทรานแซคชันเช่นฐานข้อมูลที่มีคุณสมบัติ commit / rollback การคอมมิชชันนั้นเป็นการกระทำปกติของโค้ด
จากนั้น "พื้นฐาน" คือการรับประกันอย่างน้อยที่สุดที่คุณควรเสนอ C ++ เป็นภาษาที่แข็งแกร่งมากที่มีขอบเขตทำให้คุณสามารถหลีกเลี่ยงการรั่วไหลของทรัพยากรใด ๆ (สิ่งที่ตัวรวบรวมขยะจะพบว่ามันยากที่จะนำเสนอสำหรับฐานข้อมูลการเชื่อมต่อหรือการจัดการไฟล์)
ดังนั้นเท่าที่ผมเห็นมันเป็นมูลค่ามัน
แก้ไข 2010-01-29: เกี่ยวกับการแลกเปลี่ยนที่ไม่ทุ่มตลาด
nobar แสดงความคิดเห็นที่ฉันเชื่อว่ามีความเกี่ยวข้องมากเพราะเป็นส่วนหนึ่งของ "คุณจะเขียนโค้ดที่ปลอดภัยได้อย่างไร":
- [me] การแลกเปลี่ยนจะไม่ล้มเหลว (อย่าแม้แต่เขียน swap การขว้างปา)
- [nobar] นี่เป็นคำแนะนำที่ดีสำหรับ
swap()
ฟังก์ชั่นที่เขียนขึ้นเอง ควรสังเกตว่าstd::swap()
สามารถล้มเหลวตามการดำเนินการที่ใช้ภายใน
ค่าเริ่มต้นstd::swap
จะทำสำเนาและการมอบหมายซึ่งสำหรับบางวัตถุสามารถโยนได้ ดังนั้นการสลับค่าเริ่มต้นอาจส่งผ่านไม่ว่าจะใช้สำหรับคลาสของคุณหรือแม้แต่กับคลาส STL เท่าที่ซี ++ มาตรฐานเป็นห่วงการดำเนินการแลกเปลี่ยนสำหรับvector
, deque
และlist
จะไม่โยนในขณะที่มันอาจจะสำหรับmap
ถ้า functor เปรียบเทียบสามารถโยนในการก่อสร้างสำเนา (ดูc ++ เขียนโปรแกรมภาษา, รุ่นพิเศษ, ภาคผนวก E, E.4.3 .Swap )
ดูการใช้ Visual C ++ 2008 ในการสลับของเวกเตอร์การสลับของเวกเตอร์จะไม่โยนหากเวกเตอร์สองตัวมีตัวจัดสรรเดียวกัน (เช่นกรณีปกติ) แต่จะทำสำเนาหากพวกเขามีตัวจัดสรรต่างกัน และฉันคิดว่ามันน่าจะโยนได้ในกรณีนี้
ดังนั้นข้อความต้นฉบับยังคงอยู่: ไม่เคยเขียน swap การขว้างปา แต่ต้องจำความคิดเห็นของผู้มีเกียรติ: ให้แน่ใจว่าวัตถุที่คุณแลกเปลี่ยนมีการแลกเปลี่ยนที่ไม่ใช่การขว้างปา
แก้ไข 2011-11-06: บทความที่น่าสนใจ
Dave Abrahamsผู้ให้การค้ำประกันพื้นฐาน / แข็งแรง / ไม่ตัดสัญญาให้เราอธิบายในบทความประสบการณ์ของเขาเกี่ยวกับการทำให้ข้อยกเว้น STL ปลอดภัย:
http://www.boost.org/community/exception_safety.html
ดูจุดที่ 7 (การทดสอบอัตโนมัติเพื่อความปลอดภัยยกเว้น) ซึ่งเขาใช้การทดสอบหน่วยอัตโนมัติเพื่อให้แน่ใจว่าทุกกรณีได้รับการทดสอบ ฉันเดาว่าส่วนนี้เป็นคำตอบที่ยอดเยี่ยมสำหรับคำถามของผู้เขียน " คุณแน่ใจหรือไม่ว่ามันเป็นอย่างนั้น "
แก้ไข 2013-05-31: ความคิดเห็นจากdionadar
t.integer += 1;
จะไม่มีการรับประกันว่าล้นจะไม่เกิดขึ้นไม่ปลอดภัยยกเว้นและในความเป็นจริงอาจเรียก UB ในทางเทคนิค! (ล้นลงนามคือ UB: C ++ 11 5/4 "ถ้าในระหว่างการประเมินผลของนิพจน์ผลที่ได้คือไม่ได้กำหนดทางคณิตศาสตร์หรือไม่อยู่ในช่วงของค่าที่สามารถแทนได้สำหรับประเภทพฤติกรรมที่ไม่ได้กำหนด") โปรดทราบว่าไม่ได้ลงนาม จำนวนเต็มไม่ล้น แต่ทำการคำนวณในโมดูโลคลาสสมมูล 2 ^ # บิต
Dionadar อ้างถึงบรรทัดต่อไปนี้ซึ่งมีพฤติกรรมที่ไม่ได้กำหนดแน่นอน
t.integer += 1 ; // 1. nothrow/nofail
วิธีแก้ปัญหาที่นี่คือการตรวจสอบว่าจำนวนเต็มมีค่าสูงสุด (ใช้std::numeric_limits<T>::max()
) แล้วก่อนที่จะทำการเพิ่ม
ข้อผิดพลาดของฉันจะอยู่ในส่วน "ความล้มเหลวปกติเทียบกับข้อผิดพลาด" นั่นคือข้อผิดพลาด มันไม่ได้ทำให้เหตุผลที่ทำให้เป็นโมฆะและมันก็ไม่ได้หมายความว่ารหัสยกเว้นที่ปลอดภัยนั้นไร้ประโยชน์เพราะเป็นไปไม่ได้ คุณไม่สามารถป้องกันตนเองจากการปิดคอมพิวเตอร์หรือคอมไพล์บักหรือแม้แต่บั๊กของคุณหรือข้อผิดพลาดอื่น ๆ คุณไม่สามารถบรรลุความสมบูรณ์แบบได้ แต่คุณสามารถพยายามเข้าใกล้ให้ได้มากที่สุด
ฉันแก้ไขรหัสโดยคำนึงถึงความคิดเห็นของ Dionadar