Bạn (thực sự) viết mã an toàn ngoại lệ? [đóng cửa]


312

Xử lý ngoại lệ (EH) dường như là tiêu chuẩn hiện tại và bằng cách tìm kiếm trên web, tôi không thể tìm thấy bất kỳ ý tưởng hoặc phương pháp mới nào cố gắng cải thiện hoặc thay thế nó (tốt, một số biến thể tồn tại, nhưng không có gì mới lạ).

Mặc dù hầu hết mọi người dường như bỏ qua nó hoặc chỉ chấp nhận nó, EH một số nhược điểm lớn: ngoại lệ là vô hình đối với mã và nó tạo ra nhiều, nhiều điểm thoát có thể. Joel trên phần mềm đã viết một bài báo về nó . Sự so sánh để gotophù hợp hoàn hảo, nó khiến tôi suy nghĩ lại về EH.

Tôi cố gắng tránh EH và chỉ sử dụng các giá trị trả về, gọi lại hoặc bất cứ điều gì phù hợp với mục đích. Nhưng khi bạn phải viết mã đáng tin cậy, bạn không thể bỏ qua EH những ngày này : Nó bắt đầu bằng new, có thể đưa ra một ngoại lệ, thay vì chỉ trả về 0 (như ngày xưa). Điều này làm cho bất kỳ dòng mã C ++ nào dễ bị ngoại lệ. Và sau đó, nhiều vị trí hơn trong mã cơ sở C ++ đưa ra các ngoại lệ ... std lib làm điều đó, v.v.

Cảm giác này giống như đi trên mặt đất run rẩy .. Vì vậy, bây giờ chúng tôi buộc phải quan tâm đến các ngoại lệ!

Nhưng nó khó, nó thực sự khó. Bạn phải học cách viết mã an toàn ngoại lệ, và ngay cả khi bạn có một số kinh nghiệm với nó, nó vẫn sẽ được yêu cầu kiểm tra lại bất kỳ dòng mã nào để an toàn! Hoặc bạn bắt đầu đặt các khối try / Catch ở khắp mọi nơi, làm cho mã bị khóa cho đến khi nó đạt đến trạng thái không thể đọc được.

EH đã thay thế cách tiếp cận xác định sạch cũ (giá trị trả về ..), chỉ có một vài nhược điểm dễ hiểu và dễ giải quyết bằng cách tiếp cận tạo ra nhiều điểm thoát có thể có trong mã của bạn và nếu bạn bắt đầu viết mã bắt ngoại lệ (những gì bạn bị buộc phải làm tại một số điểm), sau đó nó thậm chí còn tạo ra vô số đường dẫn thông qua mã của bạn (mã trong các khối bắt, hãy nghĩ về một chương trình máy chủ nơi bạn cần các phương tiện ghi nhật ký khác ngoài std :: cerr ..). EH có lợi thế, nhưng đó không phải là điểm chính.

Câu hỏi thực tế của tôi:

  • Bạn có thực sự viết mã an toàn ngoại lệ?
  • 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à?
  • 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?

44
"EH thay thế phương pháp xác định sạch cũ (giá trị trả về ..)" Cái gì? Các ngoại lệ chỉ mang tính quyết định như mã trả về. Không có gì ngẫu nhiên về họ.
rlbond

2
EH đã trở thành tiêu chuẩn trong một thời gian dài (kể từ Ada thậm chí!)

12
"EH" có phải là tên viết tắt được thiết lập để xử lý ngoại lệ không? Tôi chưa bao giờ nhìn thấy nó trước đây.
Thomas Padron-McCarthy

3
Luật của Newton ngày nay được áp dụng cho hầu hết mọi thứ. Việc họ dự đoán một loạt các hiện tượng trong nhiều thế kỷ là rất đáng kể.
David Thornley

4
@frunsi: Heh, xin lỗi vì đã làm bạn bối rối! Nhưng tôi nghĩ rằng, nếu bất cứ điều gì, một từ viết tắt hoặc viết tắt lạ và không giải thích được sẽ làm mọi người nản lòng hơn là không.
Thomas Padron-McCarthy

Câu trả lời:


520

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::threadhoặ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 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ụ: newcó 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_backné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:

  1. 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.
  2. 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
  3. 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 ++)
  4. 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ì tkhô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, tkhông được sửa đổi và Xsẽ 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 t2củ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 đó, tvẫ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 tt2. 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 Tkhô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à tcó giá trị bị loại trừ. Nếu thất bại, thì tvẫ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ó 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::swapsẽ 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, dequelistsẽ không ném, trong khi nó có thể cho mapnế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.


6
Cảm ơn rất nhiều! Tôi vẫn hy vọng điều gì đó khác biệt. Nhưng tôi chấp nhận câu trả lời của bạn cho việc gắn bó với C ++ và cho lời giải thích chi tiết của bạn. Bạn thậm chí còn từ chối "Điều này làm cho bất kỳ dòng mã C ++ nào dễ bị ngoại lệ". Cuối cùng, phân tích từng dòng có ý nghĩa ... Tôi phải suy nghĩ về nó.
Frunsi

8
"Một trao đổi sẽ không bao giờ thất bại". Đây là một khuyến nghị tốt cho các hàm hoán đổi () được viết tùy chỉnh. Tuy nhiên, cần lưu ý rằng std :: exchange () có thể thất bại dựa trên các hoạt động mà nó sử dụng nội bộ.
tộc

7
@Mehrdad :: "finally" does exactly what you were trying to achieveTất nhiên là có. Và bởi vì nó dễ tạo ra mã dễ vỡ finally, nên khái niệm "thử với tài nguyên" cuối cùng đã được giới thiệu trong Java 7 (nghĩa là, 10 năm sau C # usingvà 3 thập kỷ sau các hàm hủy C ++). Đây là sự giòn giã này tôi chỉ trích. Về phần Just because [finally] doesn't match your taste (RAII doesn't match mine, [...]) doesn't mean it's "failing": Trong trường hợp này, ngành công nghiệp không đồng ý với sở thích của bạn, vì các ngôn ngữ được thu gom rác có xu hướng thêm các câu lệnh lấy cảm hứng từ RAII (C # usingvà Java try).
paercebal

5
@Mehrdad :: RAII doesn't match mine, since it needs a new struct every single darn time, which is tedious sometimesKhông, không. Bạn có thể sử dụng con trỏ thông minh hoặc các lớp tiện ích để "bảo vệ" tài nguyên.
paercebal

5
@Mehrdad: "nó buộc bạn phải tạo một trình bao bọc cho thứ gì đó mà bạn có thể không cần trình bao bọc" - khi mã hóa theo kiểu RAII, bạn sẽ không cần bất kỳ loại trình bao bọc nào: ressource là đối tượng và thời gian tồn tại của đối tượng là tài nguyên. Tuy nhiên, tôi đến từ một thế giới C ++, tôi hiện đang vật lộn với một dự án Java. So với RAII trong C ++, "quản lý tài nguyên thủ công" trong Java LÀ MESS! Ý kiến ​​của tôi, nhưng Java từ lâu đã trao đổi "mgmt bộ nhớ tự động" cho "mgmt tài nguyên tự động".
Frunsi

32

Viết mã an toàn ngoại lệ trong C ++ không phải là quá nhiều về việc sử dụng nhiều khối thử {} Catch {}. Đó là về tài liệu về loại đảm bảo mã của bạn cung cấp.

Tôi khuyên bạn nên đọc sê-ri Guru Of The Week của Herb Sutter , đặc biệt là các phần 59, 60 và 61.

Tóm lại, có ba cấp độ an toàn ngoại lệ bạn có thể cung cấp:

  • Cơ bản: Khi mã của bạn ném một ngoại lệ, mã của bạn không rò rỉ tài nguyên và các đối tượng vẫn có thể bị phá hủy.
  • Mạnh: Khi mã của bạn ném một ngoại lệ, nó sẽ giữ trạng thái của ứng dụng không thay đổi.
  • Không ném: Mã của bạn không bao giờ ném ngoại lệ.

Cá nhân tôi đã phát hiện ra những bài viết này khá muộn, vì vậy phần lớn mã C ++ của tôi chắc chắn không phải là ngoại lệ.


1
Cuốn sách "Đặc biệt C ++" của ông cũng là một cuốn sách hay. Nhưng tôi vẫn cố gắng đặt câu hỏi về khái niệm EH ...
Frunsi

8
+1 OP kết hợp xử lý các trường hợp ngoại lệ (bắt chúng) với độ an toàn ngoại lệ (thường là nhiều hơn về RAII)
jk.

Tôi nghi ngờ rằng rất ít mã C ++ sản ​​xuất là ngoại lệ an toàn.
Raedwald

18

Một số người trong chúng ta đã sử dụng ngoại lệ trong hơn 20 năm. PL / Tôi có chúng, ví dụ. Tiền đề rằng chúng là một công nghệ mới và nguy hiểm đối với tôi.


Xin đừng hiểu sai ý tôi, tôi (hoặc đang cố gắng) đặt câu hỏi cho EH. Và đặc biệt là C ++ EH. Và tôi đang tìm kiếm các lựa chọn thay thế. Có lẽ tôi phải chấp nhận nó (và tôi sẽ làm nếu đó là cách duy nhất), nhưng tôi nghĩ có thể có những lựa chọn thay thế tốt hơn. Không phải tôi nghĩ rằng khái niệm này là mới, nhưng vâng, tôi nghĩ nó thể nguy hiểm hơn việc xử lý lỗi rõ ràng bằng mã trả về ...
Frunsi

1
Nếu bạn không thích nó, đừng sử dụng nó. Đặt các khối thử xung quanh những thứ bạn cần gọi có thể ném, và phát minh lại mã lỗi, và chịu đựng những vấn đề mà nó có, trong một số trường hợp có thể chấp nhận được.
bmargulies

Ok, hoàn hảo, tôi sẽ chỉ sử dụng EH và mã lỗi, và sống với nó. Tôi là một nitwit, tôi nên tự mình đi đến giải pháp đó! ;)
Frunsi

17

Trước hết (như Neil đã nêu), SEH là Xử lý ngoại lệ có cấu trúc của Microsoft. Nó tương tự nhưng không giống với xử lý ngoại lệ trong C ++. Trong thực tế, bạn phải kích hoạt Xử lý ngoại lệ C ++ nếu bạn muốn nó trong Visual Studio - hành vi mặc định không đảm bảo rằng các đối tượng cục bộ bị hủy trong mọi trường hợp! Trong cả hai trường hợp, Xử lý ngoại lệ không thực sự khó hơn, nó chỉ khác nhau .

Bây giờ cho câu hỏi thực tế của bạn.

Bạn có thực sự viết mã an toàn ngoại lệ?

Đúng. Tôi cố gắng cho mã an toàn ngoại lệ trong mọi trường hợp. Tôi truyền giáo bằng cách sử dụng các kỹ thuật RAII để truy cập vào phạm vi tài nguyên (ví dụ: boost::shared_ptrcho bộ nhớ, boost::lock_guardđể khóa). Nói chung, việc sử dụng RAII và các kỹ thuật bảo vệ phạm vi nhất quán sẽ giúp mã an toàn ngoại lệ dễ viết hơn nhiều. Bí quyết là tìm hiểu những gì tồn tại và làm thế nào để áp dụng nó.

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?

Không. Nó an toàn như nó là. Tôi có thể nói rằng tôi đã không thấy lỗi quy trình do một ngoại lệ trong vài năm hoạt động 24/7. Tôi không mong đợi mã hoàn hảo, chỉ là mã được viết tốt. Ngoài việc cung cấp sự an toàn ngoại lệ, các kỹ thuật trên đảm bảo tính chính xác theo cách gần như không thể đạt được với try/ catchkhối. Nếu bạn đang nắm bắt mọi thứ trong phạm vi kiểm soát hàng đầu của mình (luồng, quy trình, v.v.), thì bạn có thể chắc chắn rằng bạn sẽ tiếp tục chạy khi đối mặt với các ngoại lệ ( hầu hết thời gian ). Các kỹ thuật tương tự cũng sẽ giúp bạn tiếp tục chạy chính xác khi đối mặt với các ngoại lệ không có try/ catchkhối ở mọi nơi .

Bạn thậm chí có thể chắc chắn rằng nó là?

Đúng. Bạn có thể chắc chắn bằng một kiểm toán mã kỹ lưỡng nhưng không ai thực sự làm điều đó? Đánh giá mã thường xuyên và các nhà phát triển cẩn thận đi một chặng đường dài để đến đó mặc dù.

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?

Tôi đã thử một vài biến thể trong những năm qua, chẳng hạn như trạng thái mã hóa ở các bit trên (ala HRESULTs ) hoặc vụ setjmp() ... longjmp()hack khủng khiếp đó . Cả hai điều này bị phá vỡ trong thực tế mặc dù theo những cách hoàn toàn khác nhau.


Cuối cùng, nếu bạn có thói quen áp dụng một vài kỹ thuật và suy nghĩ cẩn thận về nơi bạn thực sự có thể làm gì đó để đối phó với một ngoại lệ, bạn sẽ kết thúc với mã rất dễ đọc, ngoại lệ an toàn. Bạn có thể tổng hợp điều này bằng cách làm theo các quy tắc sau:

  • Bạn chỉ muốn xem try/ catchkhi bạn có thể làm gì đó về một ngoại lệ cụ thể
  • Bạn gần như không bao giờ muốn xem mã thô newhoặc delete
  • Eschew std::sprintf, snprintfvà các mảng nói chung - sử dụng std::ostringstreamđể định dạng và thay thế các mảng bằng std::vectorstd::string
  • Khi nghi ngờ, hãy tìm chức năng trong Boost hoặc STL trước khi tự lăn

Tôi chỉ có thể khuyên bạn nên học cách sử dụng ngoại lệ đúng cách và quên mã kết quả nếu bạn dự định viết bằng C ++. Nếu bạn muốn tránh các trường hợp ngoại lệ, bạn có thể muốn xem xét việc viết bằng ngôn ngữ khác không có chúng hoặc làm cho chúng an toàn . Nếu bạn muốn thực sự học cách sử dụng đầy đủ C ++, hãy đọc một vài cuốn sách từ Herb Sutter , Nicolai JosuttisScott Meyers .


"Hành vi mặc định không đảm bảo rằng các đối tượng cục bộ bị hủy trong mọi trường hợp" Bạn đang nói rằng cài đặt mặc định của trình biên dịch Visual Studio C ++ tạo ra mã không chính xác khi đối mặt với các ngoại lệ. Có thật vậy không?
Raedwald

"Bạn gần như không bao giờ muốn thấy một mã thô newhoặc deletemã": bởi thô tôi đoán bạn có nghĩa là bên ngoài một hàm tạo hoặc hàm hủy.
Raedwald

@Raedwald - re: VC ++: phiên bản VS2005 của VC ++ sẽ không phá hủy các đối tượng cục bộ khi ném ngoại lệ SEH. Đọc "bật Xử lý ngoại lệ C ++" . Trong VS2005, các ngoại lệ SEH không gọi các hàm hủy của các đối tượng C ++ theo mặc định. Nếu bạn gọi các hàm Win32 hoặc bất cứ thứ gì được xác định trong DLL giao diện C, thì bạn phải lo lắng về điều này vì chúng có thể (và đôi khi) sẽ ném ngoại lệ SEH theo cách của bạn.
D.Shawley

@Raedwald: re: raw : về cơ bản, deletekhông bao giờ nên được sử dụng ngoài việc thực hiện tr1::shared_ptrvà tương tự. newcó thể được sử dụng miễn là việc sử dụng nó là một cái gì đó như tr1::shared_ptr<X> ptr(new X(arg, arg));. Phần quan trọng là kết quả của việc newđi thẳng vào một con trỏ được quản lý. Các trang trên boost::shared_ptrBest Practices mô tả tốt nhất.
D.Shawley

Tại sao bạn tham khảo std :: sprintf (et al) trong bộ quy tắc của bạn về an toàn ngoại lệ? Những tài liệu này không ghi nhận rằng họ đưa ra bất kỳ trường hợp ngoại lệ nào, ví dụ en.cppreference.com/w/cpp/io/c/fprintf
mabraham

10

Không thể viết mã an toàn ngoại lệ theo giả định rằng "bất kỳ dòng nào cũng có thể ném". Việc thiết kế mã an toàn ngoại lệ phụ thuộc rất nhiều vào các hợp đồng / đảm bảo nhất định mà bạn có nghĩa vụ phải mong đợi, quan sát, tuân theo và thực hiện trong mã của mình. Nó là hoàn toàn cần thiết để có mã được đảm bảo để không bao giờ ném. Có những loại đảm bảo ngoại lệ khác.

Nói cách khác, việc tạo mã an toàn ngoại lệ ở một mức độ lớn là vấn đề thiết kế chương trình không chỉ là vấn đề của mã hóa đơn giản .


8
  • Bạn có thực sự viết mã an toàn ngoại lệ?

Vâng, tôi chắc chắn có ý định.

  • 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?

Tôi chắc chắn rằng các máy chủ 24/7 của tôi được xây dựng bằng các ngoại lệ chạy 24/7 và không bị rò rỉ bộ nhớ.

  • Bạn thậm chí có thể chắc chắn, đó là?

Rất khó để chắc chắn rằng bất kỳ mã nào là chính xác. Thông thường, người ta chỉ có thể đi theo kết quả

  • 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. Sử dụng ngoại lệ sạch hơn và dễ dàng hơn bất kỳ giải pháp thay thế nào tôi đã sử dụng trong 30 năm qua trong lập trình.


30
Câu trả lời này không có giá trị.
Matt Joiner

6
@MattJoiner Sau đó, câu hỏi không có giá trị.
Miles Rout

5

Bỏ qua sự nhầm lẫn giữa các ngoại lệ SEH và C ++, bạn cần lưu ý rằng các ngoại lệ có thể được ném bất cứ lúc nào và viết mã của bạn với ý nghĩ đó. Nhu cầu về an toàn ngoại lệ phần lớn là những gì thúc đẩy việc sử dụng RAII, con trỏ thông minh và các kỹ thuật C ++ hiện đại khác.

Nếu bạn tuân theo các mẫu được thiết lập tốt, viết mã an toàn ngoại lệ không đặc biệt khó, và trên thực tế, nó dễ hơn viết mã xử lý trả về lỗi đúng trong mọi trường hợp.


3

EH là tốt, nói chung. Nhưng cách triển khai của C ++ không thân thiện lắm vì thật khó để biết mức độ bao phủ ngoại lệ của bạn tốt như thế nào. Ví dụ, Java làm cho điều này trở nên dễ dàng, trình biên dịch sẽ có xu hướng thất bại nếu bạn không xử lý các ngoại lệ có thể xảy ra.


2
Nó tốt hơn nhiều với việc sử dụng hợp lý noexcept.
einpoklum

2

Tôi thực sự thích làm việc với Eclipse và Java (mới đối với Java), vì nó ném lỗi trong trình soạn thảo nếu bạn thiếu trình xử lý EH. Điều đó khiến mọi thứ khó khăn hơn rất nhiều để quên xử lý một ngoại lệ ...

Ngoài ra, với các công cụ IDE, nó tự động thêm khối thử / bắt hoặc khối bắt khác.


5
Đó là sự khác biệt giữa các ngoại lệ được kiểm tra (Java) và không được kiểm tra (c ++) (Java cũng có một vài trong số đó). Ưu điểm của các ngoại lệ được kiểm tra là những gì bạn đã viết, nhưng ở đó nó có nhược điểm riêng. Google cho các phương pháp khác nhau và các vấn đề khác nhau mà họ có.
David Rodríguez - dribeas

2

Một số người trong chúng ta thích các ngôn ngữ như Java buộc chúng ta phải khai báo tất cả các ngoại lệ được ném bởi các phương thức, thay vì làm cho chúng vô hình như trong C ++ và C #.

Khi được thực hiện đúng cách, các ngoại lệ sẽ vượt trội hơn so với mã trả về lỗi, nếu không vì lý do nào khác ngoài việc bạn không phải truyền lỗi cho chuỗi cuộc gọi theo cách thủ công.

Điều đó đang được nói, lập trình thư viện API cấp thấp có lẽ nên tránh xử lý ngoại lệ và dính vào các mã trả về lỗi.

Theo kinh nghiệm của tôi, thật khó để viết mã xử lý ngoại lệ sạch trong C ++. Tôi cuối cùng sử dụng new(nothrow)rất nhiều.


4
Và bạn cũng đang tránh hầu hết các thư viện tiêu chuẩn? Sử dụng new(std::nothrow)là không đủ. Nhân tiện, viết mã an toàn ngoại lệ trong C ++ dễ hơn so với Java: en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization
avakar

3
Khả năng sử dụng ngoại lệ được kiểm tra của Java rất cao. Trong thực tế, các ngôn ngữ không phải là Java, chúng KHÔNG được coi là thành công. Đây là lý do tại sao câu lệnh "ném" trong C ++ hiện bị coi là lỗi thời và C # chưa bao giờ nghiêm túc xem xét việc thực hiện chúng (đây là một lựa chọn thiết kế). Đối với Java, tài liệu sau đây có thể được khai sáng: googletesting.blogspot.com/2009/09/ Mạnh
paercebal

2
Theo kinh nghiệm của tôi, việc viết mã an toàn ngoại lệ trong C ++ không khó lắm và nói chung nó có xu hướng dẫn đến mã sạch hơn. Tất nhiên, bạn phải học cách làm điều đó.
David Thornley

2

Tôi cố gắng hết sức để viết mã an toàn ngoại lệ, vâng.

Điều đó có nghĩa là tôi cẩn thận để theo dõi những dòng có thể ném. Không phải ai cũng có thể, và điều cực kỳ quan trọng là phải ghi nhớ điều đó. Chìa khóa thực sự là để suy nghĩ và thiết kế mã của bạn để đáp ứng, đảm bảo ngoại lệ được xác định trong tiêu chuẩn.

Hoạt động này có thể được viết để cung cấp đảm bảo ngoại lệ mạnh mẽ? Tôi có phải giải quyết cho cơ bản? Những dòng nào có thể ném ngoại lệ, và làm thế nào tôi có thể đảm bảo rằng nếu chúng làm như vậy, chúng không làm hỏng đối tượng?


2
  • Bạn có thực sự viết mã an toàn ngoại lệ? [Không có những điều như vậy. Các ngoại lệ là một lá chắn giấy cho các lỗi trừ khi bạn có một môi trường được quản lý. Điều này áp dụng cho ba câu hỏi đầu tiên.]

  • 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? [Thay thế cho cái gì? Vấn đề ở đây là mọi người không tách các lỗi thực tế khỏi hoạt động chương trình bình thường. Nếu đó là hoạt động bình thường của chương trình (tức là không tìm thấy tệp), thì đó không thực sự là lỗi xử lý. Nếu đó là một lỗi thực tế, không có cách nào để 'xử lý' nó hoặc đó không phải là một lỗi thực tế. Mục tiêu của bạn ở đây là tìm ra lỗi sai và dừng bảng tính và báo lỗi, khởi động lại trình điều khiển cho máy nướng bánh mì của bạn hoặc chỉ cầu nguyện rằng máy bay phản lực có thể tiếp tục bay ngay cả khi phần mềm bị lỗi và hy vọng điều tốt nhất.]


0

Rất nhiều (tôi thậm chí sẽ nói hầu hết) mọi người làm.

Điều thực sự quan trọng về các ngoại lệ, là nếu bạn không viết bất kỳ mã xử lý nào - kết quả hoàn toàn an toàn và được xử lý tốt. Quá háo hức, nhưng an toàn.

Bạn cần chủ động mắc lỗi trong trình xử lý để có được thứ gì đó không an toàn và chỉ bắt (...) {} sẽ so sánh với việc bỏ qua mã lỗi.


4
Không đúng. Rất dễ dàng để viết mã mà không phải là ngoại lệ. Ví dụ: f = new foo(); f->doSomething(); delete f;Nếu phương thức doS Something ném một ngoại lệ, thì bạn bị rò rỉ bộ nhớ.
Kristopher Johnson

1
Sẽ không thành vấn đề khi chương trình của bạn chấm dứt, phải không? Để tiếp tục thực hiện, bạn sẽ phải chủ động nuốt ngoại lệ. Vâng, có một số trường hợp cụ thể mà việc chấm dứt mà không dọn dẹp vẫn không được chấp nhận, nhưng những tình huống như vậy cần được chăm sóc đặc biệt trong bất kỳ ngôn ngữ và phong cách lập trình nào.
ima

Bạn không thể bỏ qua các ngoại lệ (và không viết mã xử lý), trong C ++ cũng như trong mã được quản lý. Nó sẽ không an toàn, và nó sẽ không cư xử tốt. Ngoại trừ có thể một số mã đồ chơi.
Frunsi

1
Nếu bạn bỏ qua các ngoại lệ trong mã ứng dụng, chương trình vẫn có thể KHÔNG hoạt động tốt khi các nguồn tài nguyên bên ngoài có liên quan. Đúng, HĐH quan tâm đến việc đóng tay cầm tập tin, khóa, ổ cắm, v.v. Nhưng không phải mọi thứ đều được xử lý, ví dụ như nó có thể để lại các tệp không cần thiết hoặc làm hỏng các tệp trong khi ghi cho chúng, v.v. Nếu bạn bỏ qua các ngoại lệ, bạn có một vấn đề trong Java, trong C ++, bạn có thể làm việc với RAII (nhưng khi bạn sử dụng kỹ thuật RAII, rất có thể bạn sử dụng chúng vì bạn quan tâm đến các ngoại lệ) ...
Frunsi

3
Làm ơn đừng xoắn lời tôi. Tôi đã viết "nếu bạn không viết bất kỳ mã xử lý nào" - và tôi có nghĩa là chính xác điều đó. Để bỏ qua các ngoại lệ, bạn cần viết mã.
ima
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.