Câu hỏi của bạn đưa ra một khẳng định rằng "Viết mã an toàn ngoại lệ là rất khó". Tôi sẽ trả lời câu hỏi của bạn trước, và sau đó, trả lời câu hỏi ẩn đằng sau chúng.
Trả lời câu hỏi
Bạn có thực sự viết mã an toàn ngoại lệ?
Tất nhiên tôi làm.
Đây là những lý do Java mất rất nhiều sức hấp dẫn của nó đối với tôi như một lập trình viên C ++ (thiếu ngữ nghĩa RAII), nhưng tôi digressing: Đây là một câu hỏi ++ C.
Trên thực tế, nó là cần thiết khi bạn cần làm việc với mã STL hoặc Boost. Ví dụ, các luồng C ++ ( boost::thread
hoặc std::thread
) sẽ đưa ra một ngoại lệ để thoát một cách duyên dáng.
Bạn có chắc chắn mã "sẵn sàng sản xuất" cuối cùng của bạn là ngoại lệ an toàn không?
Bạn thậm chí có thể chắc chắn, đó là?
Viết mã an toàn ngoại lệ cũng giống như viết mã không có lỗi.
Bạn không thể chắc chắn 100% mã của bạn là ngoại lệ an toàn. Nhưng sau đó, bạn phấn đấu cho nó, sử dụng các mẫu nổi tiếng và tránh các mẫu chống nổi tiếng.
Bạn có biết và / hoặc thực sự sử dụng các lựa chọn thay thế hoạt động?
Không có lựa chọn thay thế khả thi nào trong C ++ (tức là bạn sẽ cần quay lại C và tránh các thư viện C ++, cũng như những bất ngờ bên ngoài như Windows SEH).
Viết mã an toàn ngoại lệ
Để viết mã an toàn ngoại lệ, trước tiên bạn phải biết mức độ an toàn ngoại lệ mỗi hướng dẫn bạn viết là gì.
Ví dụ: new
có thể đưa ra một ngoại lệ, nhưng việc gán một tích hợp (ví dụ: int hoặc con trỏ) sẽ không thành công. Một trao đổi sẽ không bao giờ thất bại (đừng bao giờ viết một trao đổi ném), một std::list::push_back
ném có thể ...
Đảm bảo ngoại lệ
Điều đầu tiên cần hiểu là bạn phải có khả năng đánh giá bảo đảm ngoại lệ được cung cấp bởi tất cả các chức năng của bạn:
- none : Mã của bạn không bao giờ nên cung cấp điều đó. Mã này sẽ rò rỉ mọi thứ, và phá vỡ ở ngoại lệ đầu tiên được ném.
- cơ bản : Đây là sự đảm bảo mà bạn ít nhất phải cung cấp, nghĩa là, nếu một ngoại lệ được ném ra, không có tài nguyên nào bị rò rỉ và tất cả các đối tượng vẫn còn nguyên
- mạnh : Quá trình xử lý sẽ thành công hoặc ném ngoại lệ, nhưng nếu nó ném, thì dữ liệu sẽ ở trạng thái giống như khi quá trình xử lý chưa bắt đầu (điều này mang lại sức mạnh giao dịch cho C ++)
- nothrow / nofail : Quá trình xử lý sẽ thành công.
Ví dụ về mã
Đoạn mã sau có vẻ như đúng C ++, nhưng trên thực tế, cung cấp bảo đảm "không", và do đó, nó không đúng:
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
}
Tôi viết tất cả các mã của tôi với loại phân tích này trong tâm trí.
Bảo đảm thấp nhất được cung cấp là cơ bản, nhưng sau đó, thứ tự của mỗi lệnh làm cho toàn bộ hàm "không", bởi vì nếu 3. ném, x sẽ bị rò rỉ.
Điều đầu tiên cần làm là làm cho hàm "cơ bản", đó là đặt x vào một con trỏ thông minh cho đến khi nó được sở hữu một cách an toàn trong danh sách:
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
}
Bây giờ, mã của chúng tôi cung cấp một bảo đảm "cơ bản". Không có gì sẽ rò rỉ, và tất cả các đối tượng sẽ ở trong một trạng thái chính xác. Nhưng chúng tôi có thể cung cấp nhiều hơn, đó là sự đảm bảo mạnh mẽ. Đây là nơi nó có thể trở nên tốn kém, và đây là lý do tại sao không phải tất cả các mã C ++ đều mạnh. Hãy thử nó:
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
}
Chúng tôi đã sắp xếp lại các hoạt động, đầu tiên tạo và thiết lập X
đúng giá trị của nó. Nếu bất kỳ thao tác nào thất bại, thì t
không được sửa đổi, do đó, thao tác 1 đến 3 có thể được coi là "mạnh": Nếu một cái gì đó ném, t
không được sửa đổi và X
sẽ không bị rò rỉ vì nó thuộc sở hữu của con trỏ thông minh.
Sau đó, chúng tôi tạo ra một bản sao t2
của t
, và làm việc trên bản sao này từ hoạt động từ 4 đến 7. Nếu một cái gì đó ném, t2
được sửa đổi, nhưng sau đó, t
vẫn là bản gốc. Chúng tôi vẫn cung cấp sự đảm bảo mạnh mẽ.
Sau đó, chúng tôi trao đổi t
và t2
. Các hoạt động hoán đổi không nên chuyển sang C ++, vì vậy, hãy hy vọng trao đổi mà bạn đã viết T
không phải là quá hạn (nếu không, hãy viết lại để nó không bị mất).
Vì vậy, nếu chúng ta đạt đến cuối hàm, mọi thứ đã thành công (Không cần loại trả về) và t
có giá trị bị loại trừ. Nếu thất bại, thì t
vẫn còn giá trị ban đầu của nó.
Bây giờ, việc cung cấp bảo đảm mạnh có thể khá tốn kém, vì vậy đừng cố gắng cung cấp bảo đảm mạnh cho tất cả mã của bạn, nhưng nếu bạn có thể làm điều đó mà không phải trả chi phí (và nội tuyến C ++ và tối ưu hóa khác có thể làm cho tất cả các mã trên không tốn kém) , sau đó làm điều đó. Người dùng chức năng sẽ cảm ơn bạn cho nó.
Phần kết luận
Cần có một số thói quen để viết mã an toàn ngoại lệ. Bạn sẽ cần đánh giá bảo lãnh được cung cấp bởi mỗi hướng dẫn bạn sẽ sử dụng và sau đó, bạn sẽ cần đánh giá bảo lãnh được cung cấp bởi một danh sách các hướng dẫn.
Tất nhiên, trình biên dịch C ++ sẽ không sao lưu bảo đảm (trong mã của tôi, tôi cung cấp bảo đảm dưới dạng thẻ @warning doxygen), điều này hơi buồn, nhưng nó không ngăn bạn cố gắng viết mã an toàn ngoại lệ.
Lỗi bình thường so với lỗi
Làm thế nào một lập trình viên có thể đảm bảo rằng một hàm không bị lỗi sẽ luôn thành công? Rốt cuộc, chức năng có thể có một lỗi.
Đây là sự thật. Các đảm bảo ngoại lệ được cho là được cung cấp bởi mã không có lỗi. Nhưng sau đó, trong bất kỳ ngôn ngữ nào, việc gọi một chức năng cho rằng chức năng này không có lỗi. Không có mã lành mạnh nào bảo vệ chính nó trước khả năng nó có lỗi. Viết mã tốt nhất bạn có thể, và sau đó, đưa ra sự đảm bảo với giả định rằng nó không có lỗi. Và nếu có lỗi, hãy sửa nó.
Các ngoại lệ dành cho lỗi xử lý đặc biệt, không phải lỗi mã.
Những từ cuối
Bây giờ, câu hỏi là "Điều này có đáng không?".
Tất nhiên là thế rồi. Có chức năng "nothrow / no-fail" biết rằng chức năng sẽ không thất bại là một lợi ích tuyệt vời. Điều tương tự cũng có thể nói đối với chức năng "mạnh", cho phép bạn viết mã với ngữ nghĩa giao dịch, như cơ sở dữ liệu, với các tính năng cam kết / rollback, cam kết là thực thi mã thông thường, ném ngoại lệ là rollback.
Sau đó, "cơ bản" là sự đảm bảo tối thiểu mà bạn nên cung cấp. C ++ là một ngôn ngữ rất mạnh ở đó, với phạm vi của nó, cho phép bạn tránh mọi rò rỉ tài nguyên (điều mà một người thu gom rác sẽ gặp khó khăn khi cung cấp cho cơ sở dữ liệu, kết nối hoặc xử lý tệp).
Vì vậy, như xa như tôi nhìn thấy nó, nó là giá trị nó.
Chỉnh sửa 2010-01-29: Về trao đổi không ném
nobar đã đưa ra một nhận xét mà tôi tin rằng, khá phù hợp, bởi vì đó là một phần của "làm thế nào để bạn viết mã an toàn ngoại lệ":
- [tôi] Một trao đổi sẽ không bao giờ thất bại (thậm chí không viết một trao đổi ném)
- [nobar] Đây là một khuyến nghị tốt cho các
swap()
chức năng được viết tùy chỉnh . Tuy nhiên, cần lưu ý rằng std::swap()
có thể thất bại dựa trên các hoạt động mà nó sử dụng nội bộ
mặc định std::swap
sẽ tạo các bản sao và bài tập, mà, đối với một số đối tượng, có thể ném. Do đó, trao đổi mặc định có thể ném, được sử dụng cho các lớp của bạn hoặc thậm chí cho các lớp STL. Theo như chuẩn C ++ là có liên quan, nghiệp vụ hoán đổi cho vector
, deque
và list
sẽ không ném, trong khi nó có thể cho map
nếu functor so sánh có thể ném vào xây dựng bản sao (Xem C ++ Lập trình Ngôn ngữ, phiên bản đặc biệt, phụ lục E, E.4.3 .Swap ).
Nhìn vào Visual C ++ 2008 thực hiện hoán đổi của vectơ, hoán đổi của vectơ sẽ không ném nếu hai vectơ có cùng cấp phát (nghĩa là trường hợp bình thường), nhưng sẽ tạo bản sao nếu chúng có các cấp phát khác nhau. Và do đó, tôi cho rằng nó có thể ném trong trường hợp cuối cùng này.
Vì vậy, văn bản gốc vẫn giữ nguyên: Đừng bao giờ viết một trao đổi ném, nhưng phải ghi nhớ bình luận cao quý: Hãy chắc chắn rằng các đối tượng bạn trao đổi có hoán đổi không ném.
Chỉnh sửa 2011-11-06: Bài viết thú vị
Dave Abrahams , người đã cho chúng tôi các đảm bảo cơ bản / mạnh mẽ / không hạn chế , được mô tả trong một bài viết kinh nghiệm của anh ấy về việc làm cho ngoại lệ STL an toàn:
http://www.boost.org/community/exception_safe.html
Nhìn vào điểm thứ 7 (Kiểm tra tự động về an toàn ngoại lệ), trong đó anh ta dựa vào kiểm tra đơn vị tự động để đảm bảo mọi trường hợp đều được kiểm tra. Tôi đoán phần này là một câu trả lời tuyệt vời cho câu hỏi của tác giả " Bạn có thể chắc chắn, đó là? ".
Chỉnh sửa 2013-05-31: Nhận xét từ dionadar
t.integer += 1;
là không có đảm bảo rằng tràn sẽ không xảy ra KHÔNG ngoại lệ an toàn, và trên thực tế có thể gọi kỹ thuật UB! (Tràn đã ký là UB: C ++ 11 5/4 "Nếu trong quá trình đánh giá biểu thức, kết quả không được xác định theo toán học hoặc không nằm trong phạm vi giá trị đại diện cho loại của nó, hành vi không được xác định.") số nguyên không tràn, nhưng thực hiện các phép tính của chúng trong một lớp modulo tương đương 2 ^ # bit.
Dionadar đang đề cập đến dòng sau, thực sự có hành vi không xác định.
t.integer += 1 ; // 1. nothrow/nofail
Giải pháp ở đây là xác minh xem số nguyên đã ở giá trị tối đa (sử dụng std::numeric_limits<T>::max()
) chưa trước khi thực hiện phép cộng.
Lỗi của tôi sẽ xảy ra trong phần "Lỗi bình thường so với lỗi", đó là lỗi. Nó không làm mất hiệu lực lý do và điều đó không có nghĩa là mã an toàn ngoại lệ là vô dụng vì không thể đạt được. Bạn không thể tự bảo vệ mình khỏi việc tắt máy tính, hoặc lỗi trình biên dịch, hoặc thậm chí là lỗi của bạn hoặc các lỗi khác. Bạn không thể đạt được sự hoàn hảo, nhưng bạn có thể cố gắng đến gần nhất có thể.
Tôi đã sửa mã với nhận xét của Dionadar.