Thử bắt hoặc ifs để xử lý lỗi trong C ++


30

Là các ngoại lệ được sử dụng rộng rãi trong thiết kế công cụ trò chơi hoặc tốt hơn là sử dụng thuần túy các câu lệnh if? Ví dụ với các ngoại lệ:

try {
    m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
    m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
    m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
    m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
    m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
    m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
} catch ... {
    // some info about error
}

và với ifs:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
if( m_fpsTextId == -1 ) {
    // show error
}
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
if( m_cpuTextId == -1 ) {
    // show error
}
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
if( m_frameTimeTextId == -1 ) {
    // show error
}
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
if( m_mouseCoordTextId == -1 ) {
    // show error
}
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
if( m_cameraPosTextId == -1 ) {
    // show error
}
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
if( m_cameraRotTextId == -1 ) {
    // show error
}

Tôi nghe nói rằng các ngoại lệ chậm hơn một chút so với if và tôi không nên trộn lẫn các ngoại lệ với phương pháp kiểm tra if. Tuy nhiên, với các ngoại lệ, tôi có nhiều mã dễ đọc hơn so với hàng tấn if sau mỗi phương thức khởi tạo () hoặc một cái gì đó tương tự, mặc dù đôi khi chúng quá nặng đối với chỉ một phương thức. Họ có ổn để phát triển trò chơi hay tốt hơn là gắn bó với ifs đơn giản?


Nhiều người cho rằng Boost không tốt cho phát triển trò chơi, nhưng tôi khuyên bạn nên xem ["Boost.optional"] ( boost.org/doc/libs/1_52_0/libs/optional/doc/html/index.html ) nó sẽ làm ví dụ ifs của bạn đẹp hơn một chút
ManicQin

Có một cuộc nói chuyện thú vị về việc xử lý lỗi của Andrei Alexandrescu, tôi đã sử dụng một cách tiếp cận tương tự với anh ta mà không có ngoại lệ.
Thelvyn

Câu trả lời:


48

Câu trả lời ngắn: đọc Xử lý lỗi hợp lý 1 , Xử lý lỗi nhạy cảm 2Xử lý lỗi hợp lý 3 của Niklas Frykholm. Trên thực tế, hãy đọc tất cả các bài viết trên blog đó, trong khi bạn đang ở đó. Không nói tôi đồng ý với tất cả mọi thứ, nhưng hầu hết là vàng.

Đừng sử dụng ngoại lệ. Có rất nhiều lý do. Tôi sẽ liệt kê những cái chính.

Chúng thực sự có thể chậm hơn, mặc dù điều đó được giảm thiểu khá nhiều trên các trình biên dịch mới hơn. Một số trình biên dịch hỗ trợ "ngoại lệ không chi phí" cho các đường dẫn mã không thực sự kích hoạt ngoại lệ (mặc dù đó là một chút dối trá, vì vẫn còn dữ liệu bổ sung mà xử lý ngoại lệ yêu cầu, làm tăng kích thước thực thi / dll của bạn). Mặc dù vậy, kết quả cuối cùng là có, sử dụng các ngoại lệ chậm hơn và trong bất kỳ đường dẫn mã quan trọng nào về hiệu năng , bạn tuyệt đối nên tránh chúng. Tùy thuộc vào trình biên dịch của bạn, việc bật chúng ở tất cả có thể thêm chi phí. Chúng luôn luôn phình to kích thước mã, khá đáng kể trong nhiều trường hợp, điều này có thể ảnh hưởng nghiêm trọng đến hiệu suất trên phần cứng ngày nay.

Trường hợp ngoại lệ làm cho mã nhiều hơn mong manh. Có một hình ảnh khét tiếng (đáng buồn là tôi không thể tìm thấy ngay bây giờ) về cơ bản chỉ hiển thị trên biểu đồ về sự khó khăn khi viết mã an toàn ngoại lệ so với mã không an toàn ngoại lệ và trước đây là một thanh lớn hơn đáng kể. Đơn giản là có rất nhiều vấn đề nhỏ với các ngoại lệ và nhiều cách để viết mã có vẻ ngoại lệ an toàn nhưng thực sự không phải vậy. Ngay cả toàn bộ ủy ban C ++ 11 cũng đồng ý với điều này và quên thêm một hàm trợ giúp quan trọng để sử dụng std :: unique_ptr một cách chính xác và ngay cả với các hàm trợ giúp đó, cũng cần phải gõ nhiều hơn để sử dụng chúng hơn là không và hầu hết các lập trình viên đã thắng ' thậm chí không nhận ra điều gì sai nếu họ không.

Cụ thể hơn đối với ngành công nghiệp trò chơi, các trình biên dịch / thời gian chạy do nhà cung cấp cung cấp hoàn toàn không hỗ trợ các trường hợp ngoại lệ, hoặc thậm chí không hỗ trợ chúng. Nếu mã của bạn sử dụng ngoại lệ, có thể bạn vẫn còn trong thời đại ngày nay và phải viết lại các phần của mã để chuyển mã sang nền tảng mới. .

Dòng suy nghĩ chung là khá rõ ràng: sử dụng ngoại lệ cho các trường hợp đặc biệt . Sử dụng chúng khi chương trình của bạn rơi vào tình trạng "Tôi không biết phải làm gì, có thể hy vọng người khác làm vậy, vì vậy tôi sẽ ném một ngoại lệ và xem điều gì sẽ xảy ra." Sử dụng chúng khi không có lựa chọn khác có ý nghĩa. Sử dụng chúng khi bạn không quan tâm nếu bạn vô tình làm rò rỉ một ít bộ nhớ hoặc không dọn sạch tài nguyên do bạn làm hỏng việc sử dụng tay cầm thông minh phù hợp. Trong tất cả các trường hợp khác, không sử dụng chúng.

Về mã như ví dụ của bạn, bạn có một số cách khác để khắc phục vấn đề. Một trong những điều mạnh mẽ hơn - mặc dù không nhất thiết là lý tưởng nhất trong ví dụ đơn giản của bạn - là nghiêng về các loại lỗi đơn âm. Nghĩa là, createdText () có thể trả về loại xử lý tùy chỉnh thay vì số nguyên. Loại tay cầm này có bộ truy cập để cập nhật hoặc kiểm soát văn bản. Nếu tay cầm được đưa vào trạng thái lỗi (vì createdText () không thành công) thì các cuộc gọi tiếp theo đến tay cầm chỉ đơn giản là thất bại. Bạn cũng có thể truy vấn tay cầm để xem nó có bị lỗi không, và nếu vậy, lỗi cuối cùng là gì. Cách tiếp cận này có nhiều chi phí hơn các lựa chọn khác, nhưng nó khá chắc chắn. Sử dụng nó trong các trường hợp khi bạn cần thực hiện một chuỗi hoạt động dài trong một số ngữ cảnh trong đó bất kỳ thao tác đơn lẻ nào cũng có thể thất bại trong sản xuất, nhưng ở đó bạn không / không thể / thắng '

Một cách khác để thực hiện xử lý lỗi đơn âm là, thay vì sử dụng các đối tượng xử lý tùy chỉnh, làm cho các phương thức trên đối tượng bối cảnh xử lý một cách duyên dáng với các id xử lý không hợp lệ. Ví dụ: nếu createdText () trả về -1 khi không thành công, thì bất kỳ lệnh gọi nào khác đến m_statistic có một trong các điều khiển đó sẽ thoát một cách duyên dáng nếu -1 được truyền vào.

Bạn cũng có thể đặt in lỗi bên trong chức năng thực sự bị lỗi. Trong ví dụ của bạn, createdText () có thể có nhiều thông tin hơn về sự cố, do đó, nó sẽ có thể đưa một lỗi có ý nghĩa hơn vào nhật ký. Trong trường hợp này có rất ít lợi ích để đẩy việc xử lý lỗi / in ra cho người gọi. Làm điều đó khi người gọi có nhu cầu tùy chỉnh xử lý (hoặc sử dụng tiêm phụ thuộc). Lưu ý rằng có một bảng điều khiển trong trò chơi có thể bật lên bất cứ khi nào có lỗi được ghi lại là một ý tưởng hay và cũng giúp ở đây.

Tùy chọn tốt nhất (đã được trình bày trong loạt bài viết được liên kết ở trên) cho các cuộc gọi mà bạn không mong đợi thất bại trong bất kỳ môi trường lành mạnh nào - như hành động đơn giản tạo các đốm văn bản cho hệ thống thống kê - là chỉ có chức năng đã thất bại (tạo ra trong ví dụ của bạn) hủy bỏ. Bạn có thể chắc chắn một cách hợp lý rằng createdText () sẽ không bị lỗi trong quá trình sản xuất trừ khi có thứ gì đó hoàn toàn bị loại bỏ (ví dụ: người dùng đã xóa các tệp dữ liệu phông chữ hoặc vì một lý do nào đó chỉ có 256 MB bộ nhớ, v.v.). Trong nhiều trường hợp, thậm chí không có việc gì để làm khi thất bại xảy ra. Hết bộ nhớ? Bạn thậm chí có thể không thể thực hiện phân bổ cần thiết để tạo một bảng GUI đẹp để hiển thị cho người dùng lỗi OOM. Thiếu phông chữ? Làm cho nó khó hiển thị lỗi cho người dùng. Sao cũng được

Chỉ cần sự cố là hoàn toàn tốt, miễn là bạn (a) ghi lỗi vào tệp nhật ký và (b) chỉ thực hiện với các lỗi không phải do hành động của người dùng thông thường.

Tôi sẽ không nói điều tương tự ở tất cả các ứng dụng máy chủ, trong đó tính khả dụng là rất quan trọng và giám sát cơ quan giám sát là không đủ, nhưng điều đó hoàn toàn khác so với phát triển ứng dụng khách trò chơi . Tôi cũng chắc chắn sẽ giúp bạn tránh sử dụng C / C ++ vì các cơ sở xử lý ngoại lệ của các ngôn ngữ khác có xu hướng không cắn như C ++ vì chúng là môi trường được quản lý và không có tất cả các vấn đề an toàn ngoại lệ mà C ++ gặp phải. Bất kỳ mối quan tâm về hiệu suất cũng được giảm nhẹ khi các máy chủ có xu hướng tập trung nhiều hơn vào tính song song và thông lượng hơn là đảm bảo độ trễ tối thiểu như các máy khách trò chơi. Ngay cả các máy chủ trò chơi hành động dành cho game bắn súng và tương tự cũng có thể hoạt động khá tốt khi được viết bằng C #, vì họ hiếm khi đẩy phần cứng đến giới hạn của nó như các máy khách FPS có xu hướng làm.


1
+1. Ngoài ra, có khả năng thêm thuộc tính like warn_unused_resultvào chức năng trong một số trình biên dịch cho phép bắt mã lỗi không được xử lý.
Maciej Piechotka

Đến đây, thấy C ++ trong tiêu đề và hoàn toàn chắc chắn việc xử lý lỗi đơn âm sẽ không có ở đây. Đồ tốt!
Adam

Điều gì về những nơi mà hiệu suất không quan trọng và bạn chủ yếu nhắm đến việc thực hiện nhanh chóng? ví dụ khi bạn đang thực hiện logic trò chơi?
Ali1S 232

còn ý tưởng sử dụng xử lý ngoại lệ chỉ nhằm mục đích gỡ lỗi thì sao? Giống như khi bạn phát hành trò chơi, bạn mong muốn mọi thứ diễn ra suôn sẻ mà không gặp vấn đề gì. vì vậy bạn sử dụng các ngoại lệ để tìm và sửa lỗi trong quá trình phát triển và sau đó trong chế độ phát hành, hãy loại bỏ tất cả các ngoại lệ đó.
Ali1S 232

1
@Gajoo: đối với logic trò chơi, các ngoại lệ chỉ làm cho logic khó theo dõi hơn (vì nó làm cho tất cả mã khó theo dõi hơn). Ngay cả trong trường hợp ngoại lệ của Pythnn và C # là rất hiếm đối với logic trò chơi, theo kinh nghiệm của tôi. để gỡ lỗi, khẳng định cứng thường là cách thuận tiện hơn. Phá vỡ và dừng lại tại thời điểm chính xác có gì đó không ổn, không phải sau khi tháo gỡ các phần lớn của ngăn xếp và mất tất cả các loại thông tin theo ngữ cảnh do ngoại lệ ném ngoại lệ. Để gỡ lỗi logic, bạn muốn xây dựng các công cụ và thông tin / chỉnh sửa GUI, để các nhà thiết kế có thể kiểm tra và chỉnh sửa.
Sean Middleditch

13

Sự khôn ngoan cũ của chính Donald Knuth là:

"Chúng ta nên quên đi sự thiếu hiệu quả nhỏ, nói khoảng 97% thời gian: tối ưu hóa sớm là gốc rễ của mọi tội lỗi."

In nó như một poster lớn và treo nó ra bất cứ nơi nào bạn lập trình nghiêm túc.

Vì vậy, ngay cả khi thử / bắt chậm hơn một chút thì tôi sẽ sử dụng nó theo mặc định:

  • Mã phải dễ đọc và dễ hiểu nhất có thể. Bạn có thể viết mã một lần nhưng bạn sẽ đọc nó nhiều lần nữa để gỡ lỗi mã này, để gỡ lỗi mã khác, để hiểu, để cải thiện, ...

  • Sự đúng đắn về hiệu suất. Làm các công cụ if / khác một cách chính xác không phải là chuyện nhỏ. Trong ví dụ của bạn, nó được thực hiện không chính xác, vì không thể hiển thị một lỗi mà là nhiều lỗi. Bạn phải sử dụng xếp tầng nếu / sau đó / khác.

  • Dễ dàng sử dụng nhất quán: Thử / bắt theo kiểu mà bạn không cần kiểm tra lỗi trên mỗi dòng. Mặt khác: Một thiếu nếu / khác và mã của bạn có thể phá hỏng. Tất nhiên chỉ trong trường hợp không thể làm được.

Vì vậy, điểm cuối cùng:

Tôi nghe nói rằng các ngoại lệ chậm hơn một chút so với if

Tôi đã nghe nói rằng khoảng 15 năm trước, đã không nghe thấy điều đó từ bất kỳ nguồn đáng tin cậy nào gần đây. Trình biên dịch có thể đã được cải thiện hoặc bất cứ điều gì.

Điểm chính là: Không tối ưu hóa sớm. Chỉ làm điều đó khi bạn có thể chứng minh bằng điểm chuẩn rằng mã trong tay là vòng lặp bên trong chặt chẽ của một số mã được sử dụng nhiều kiểu chuyển đổi giúp cải thiện hiệu suất đáng kể . Đây là trường hợp 3%.


22
Tôi sẽ -1 cái này đến vô cùng. Tôi không thể hiểu được tác hại mà trích dẫn Knuth này đã gây ra cho bộ não của nhà phát triển. Xem xét liệu có nên sử dụng ngoại lệ không tối ưu hóa sớm hay không, đó là một quyết định thiết kế, một quyết định thiết kế quan trọng có sự phân nhánh vượt xa hiệu suất.
sam hocevar

3
@SamHocevar: Tôi xin lỗi, tôi không hiểu ý của bạn: Câu hỏi về hiệu suất có thể đạt được khi sử dụng ngoại lệ. Quan điểm của tôi: Đừng nghĩ về nó, cú đánh không tệ (nếu có) và những thứ khác quan trọng hơn nhiều. Ở đây bạn có vẻ đồng ý bằng cách thêm "quyết định thiết kế" vào danh sách của tôi. ĐƯỢC. Mặt khác, bạn nói rằng trích dẫn của Knuth là ngụ ý xấu rằng tối ưu hóa sớm không phải là xấu. Nhưng đây chính xác là những gì đã xảy ra ở đây IMO: Q không nghĩ về kiến ​​trúc, thiết kế hoặc các thuật toán khác nhau, chỉ về các trường hợp ngoại lệ và tác động hiệu suất của chúng.
AH

2
Tôi sẽ không đồng ý với sự rõ ràng trong C ++. C ++ có các ngoại lệ không được kiểm tra trong khi một số trình biên dịch triển khai các giá trị trả về được kiểm tra. Kết quả là bạn có mã trông rõ ràng hơn nhưng có thể bị rò rỉ bộ nhớ hoặc thậm chí đặt mã ở trạng thái không xác định. Ngoài ra mã của bên thứ ba có thể không phải là ngoại lệ an toàn. (Tình huống là khác nhau trong Java / C # đã kiểm tra các ngoại lệ, GC, ...). Theo quyết định thiết kế - nếu chúng không vượt qua các điểm API, việc tái cấu trúc từ / đến từng kiểu có thể được thực hiện bán tự động với perl một lớp.
Maciej Piechotka

1
@SamHocevar: " Câu hỏi là về những gì tốt hơn, không phải những gì nhanh hơn. " Đọc lại đoạn cuối của câu hỏi. Các chỉ lý do ông thậm chí còn suy nghĩ về việc không sử dụng trường hợp ngoại lệ là bởi vì ông nghĩ rằng họ có thể chậm hơn. Bây giờ nếu bạn nghĩ rằng có những mối quan tâm khác mà OP đã không tính đến, hãy thoải mái đăng chúng hoặc nâng cao những người đã làm. Nhưng OP rất tập trung vào hiệu suất của các ngoại lệ.
Nicol Bolas

3
@AH - nếu bạn định trích dẫn Knuth thì đừng quên phần còn lại của nó (và IMO là phần quan trọng nhất): " Tuy nhiên, chúng ta không nên bỏ qua cơ hội của mình trong 3% quan trọng đó. Một lập trình viên giỏi sẽ không Lulled vào sự tự mãn bởi lý do như vậy, anh ta sẽ khôn ngoan để xem xét cẩn thận mã quan trọng, nhưng chỉ sau khi mã đó đã được xác định . " Tất cả mọi người thường coi đây là một cái cớ để không tối ưu hóa tất cả, hoặc cố gắng biện minh cho mã bị chậm trong trường hợp hiệu suất không phải là tối ưu hóa nhưng thực tế là một yêu cầu cơ bản.
Maximus Minimus

10

Đã có một câu trả lời thực sự tuyệt vời cho câu hỏi này nhưng tôi muốn thêm một số suy nghĩ về khả năng đọc mã và phục hồi lỗi trong trường hợp cụ thể của bạn.

Tôi tin rằng mã của bạn thực sự có thể trông như thế này:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );

Không có ngoại lệ, không có ifs. Báo cáo lỗi có thể được thực hiện trong createText. Và createTextcó thể trả về ID kết cấu mặc định không yêu cầu bạn kiểm tra giá trị trả về, để phần còn lại của mã hoạt động tốt.

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.