C ++ dường như thích sử dụng ngoại lệ thường xuyên hơn.
Tôi sẽ đề xuất thực sự ít hơn Objective-C ở một số khía cạnh vì thư viện chuẩn C ++ thường không gây ra các lỗi lập trình như truy cập ngoài giới hạn của chuỗi truy cập ngẫu nhiên trong mẫu thiết kế trường hợp phổ biến nhất của nó ( operator[]
ví dụ) cố gắng để vô hiệu hóa một trình vòng lặp không hợp lệ. Ngôn ngữ không ném vào việc truy cập một mảng ngoài giới hạn, hoặc hủy bỏ một con trỏ null hoặc bất cứ thứ gì thuộc loại này.
Lấy lỗi lập trình viên phần lớn ra khỏi phương trình xử lý ngoại lệ thực sự lấy đi một loại lỗi rất lớn mà các ngôn ngữ khác thường mắc phải throwing
. C ++ có xu hướng assert
(không được biên dịch trong các bản dựng phát hành / sản xuất, chỉ các bản dựng gỡ lỗi) hoặc chỉ bị trục trặc (thường gặp sự cố) trong các trường hợp như vậy, có lẽ một phần vì ngôn ngữ không muốn áp đặt chi phí cho các kiểm tra thời gian chạy như vậy như sẽ được yêu cầu để phát hiện các lỗi lập trình viên như vậy trừ khi lập trình viên đặc biệt muốn trả chi phí bằng cách viết mã thực hiện kiểm tra như vậy.
Sutter thậm chí còn khuyến khích tránh các ngoại lệ trong các trường hợp như vậy trong Tiêu chuẩn mã hóa C ++:
Nhược điểm chính của việc sử dụng một ngoại lệ để báo cáo lỗi lập trình là bạn không thực sự muốn ngăn xếp ngăn chặn xảy ra khi bạn muốn trình gỡ lỗi khởi chạy trên dòng chính xác nơi phát hiện vi phạm, với trạng thái của dòng còn nguyên vẹn. Tóm lại: Có những lỗi mà bạn biết có thể xảy ra (xem Mục 69 đến 75). Đối với mọi thứ khác không nên, và đó là lỗi của lập trình viên nếu có, đó là assert
.
Quy tắc đó không nhất thiết phải được đặt trong đá. Trong một số trường hợp quan trọng hơn về nhiệm vụ, có thể tốt hơn là sử dụng các hàm bao và một tiêu chuẩn mã hóa để ghi lại thống nhất các lỗi lập trình xảy ra và throw
khi có lỗi lập trình viên như cố gắng bảo vệ một cái gì đó không hợp lệ hoặc truy cập nó ra khỏi giới hạn, bởi vì có thể quá tốn kém để không phục hồi trong những trường hợp đó nếu phần mềm có cơ hội. Nhưng nhìn chung, việc sử dụng ngôn ngữ phổ biến hơn có xu hướng ủng hộ không ném vào những sai lầm của lập trình viên.
Ngoại lệ bên ngoài
Trường hợp tôi thấy các ngoại lệ được khuyến khích thường xuyên nhất trong C ++ (theo ủy ban tiêu chuẩn, ví dụ) là dành cho "ngoại lệ bên ngoài", như trong một kết quả không mong muốn ở một số nguồn bên ngoài chương trình. Một ví dụ là không phân bổ bộ nhớ. Một cái khác là không mở được một tệp quan trọng cần thiết để phần mềm chạy. Một cái khác là không kết nối được với một máy chủ cần thiết. Một người dùng khác đang gây nhiễu nút hủy bỏ để hủy bỏ một hoạt động có đường dẫn thực thi trường hợp chung dự kiến sẽ thành công mà không có sự gián đoạn bên ngoài này. Tất cả những điều này nằm ngoài sự kiểm soát của phần mềm ngay lập tức và các lập trình viên đã viết nó. Chúng là kết quả bất ngờ từ các nguồn bên ngoài ngăn cản hoạt động (mà thực sự nên được coi là một giao dịch không thể tách rời trong cuốn sách của tôi *) để có thể thành công.
Giao dịch
Tôi thường khuyến khích xem một try
khối là một "giao dịch" vì các giao dịch sẽ thành công toàn bộ hoặc thất bại nói chung. Nếu chúng tôi đang cố gắng làm một cái gì đó và nó thất bại nửa chừng, thì mọi tác dụng phụ / đột biến được thực hiện đối với trạng thái chương trình thường cần được khôi phục để đưa hệ thống trở lại trạng thái hợp lệ như thể giao dịch chưa bao giờ được thực hiện, giống như một RDBMS không xử lý được một nửa truy vấn sẽ không ảnh hưởng đến tính toàn vẹn của cơ sở dữ liệu. Nếu bạn thay đổi trạng thái chương trình trực tiếp trong giao dịch đã nói, thì bạn phải "không sửa đổi" khi gặp lỗi (và ở đây phạm vi bảo vệ có thể hữu ích với RAII).
Cách thay thế đơn giản hơn nhiều là không làm thay đổi trạng thái chương trình gốc; bạn có thể thay đổi một bản sao của nó và sau đó, nếu nó thành công, trao đổi bản sao với bản gốc (đảm bảo trao đổi không thể ném). Nếu thất bại, loại bỏ các bản sao. Điều này cũng áp dụng ngay cả khi bạn không sử dụng ngoại lệ để xử lý lỗi nói chung. Một tư duy "giao dịch" là chìa khóa để phục hồi thích hợp nếu đột biến trạng thái chương trình xảy ra trước khi gặp lỗi. Nó hoặc thành công như một toàn thể hoặc thất bại như toàn bộ. Nó không thành công một nửa trong việc tạo đột biến của nó.
Đây là một trong những chủ đề ít được thảo luận nhất khi tôi thấy các lập trình viên hỏi về cách xử lý lỗi hoặc xử lý ngoại lệ đúng cách, nhưng khó nhất trong tất cả các phần mềm muốn chuyển đổi trực tiếp trạng thái chương trình trong nhiều phần mềm hoạt động của nó. Sự tinh khiết và bất biến có thể giúp ở đây đạt được sự an toàn ngoại lệ cũng giống như chúng giúp cho sự an toàn của luồng, vì một tác dụng phụ đột biến / bên ngoài không xảy ra không cần phải được khôi phục.
Hiệu suất
Một yếu tố định hướng khác trong việc có nên sử dụng ngoại lệ hay không là hiệu suất và tôi không có nghĩa là theo cách ám ảnh, chèn ép, phản tác dụng. Rất nhiều trình biên dịch C ++ thực hiện cái gọi là "Xử lý ngoại lệ chi phí bằng không".
Nó cung cấp chi phí thời gian chạy bằng không cho việc thực thi không có lỗi, vượt qua cả xử lý lỗi trả về giá trị C. Như một sự đánh đổi, việc truyền bá một ngoại lệ có một chi phí lớn.
Theo những gì tôi đã đọc về nó, nó làm cho các đường dẫn thực thi trường hợp thông thường của bạn không yêu cầu chi phí (thậm chí cả chi phí thường đi kèm với việc xử lý và truyền mã lỗi kiểu C), đổi lại là rất nhiều chi phí đối với các đường dẫn đặc biệt ( có nghĩa throwing
là bây giờ đắt hơn bao giờ hết).
"Đắt tiền" hơi khó định lượng nhưng, đối với người mới bắt đầu, có lẽ bạn không muốn bị ném hàng triệu lần trong một vòng lặp chặt chẽ. Kiểu thiết kế này giả định rằng các ngoại lệ không xảy ra bên trái và bên phải mọi lúc.
Không lỗi
Và điểm hiệu suất đó đưa tôi đến những lỗi không, điều này đáng ngạc nhiên mờ nhạt nếu chúng ta nhìn vào tất cả các loại ngôn ngữ khác. Nhưng tôi sẽ nói, với thiết kế EH chi phí bằng không được đề cập ở trên, bạn gần như chắc chắn không muốn throw
phản hồi lại một khóa không được tìm thấy trong một bộ. Bởi vì không chỉ có thể là lỗi (người tìm kiếm khóa có thể đã tạo ra bộ và dự kiến sẽ tìm kiếm các khóa không tồn tại), nhưng nó sẽ rất tốn kém trong bối cảnh đó.
Ví dụ, một hàm giao nhau được thiết lập có thể muốn lặp qua hai bộ và tìm kiếm các khóa mà chúng có chung. Nếu không tìm thấy khóa threw
, bạn sẽ lặp lại và có thể gặp phải ngoại lệ trong một nửa hoặc nhiều lần lặp lại:
Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
Set<int> intersection;
for (int key: a)
{
try
{
b.find(key);
intersection.insert(other_key);
}
catch (const KeyNotFoundException&)
{
// Do nothing.
}
}
return intersection;
}
Ví dụ trên hoàn toàn vô lý và cường điệu, nhưng tôi đã thấy, trong mã sản xuất, một số người đến từ các ngôn ngữ khác sử dụng ngoại lệ trong C ++ có phần giống như thế này và tôi nghĩ rằng đây là một tuyên bố thực tế hợp lý rằng đây không phải là cách sử dụng ngoại lệ phù hợp trong C ++. Một gợi ý khác ở trên là bạn sẽ nhận thấy catch
khối này hoàn toàn không có gì để làm và chỉ được viết để bỏ qua bất kỳ trường hợp ngoại lệ nào, và đó thường là một gợi ý (mặc dù không phải là người bảo lãnh) rằng các ngoại lệ có thể không được sử dụng rất phù hợp trong C ++.
Đối với các loại trường hợp đó, một số loại giá trị trả về biểu thị thất bại (bất cứ điều gì từ việc trở lại false
trình lặp không hợp lệ nullptr
hoặc bất cứ điều gì có ý nghĩa trong ngữ cảnh) thường phù hợp hơn nhiều, và cũng thường thực tế và hiệu quả hơn vì loại không có lỗi trường hợp thường không gọi cho một số quá trình giải nén ngăn xếp để đến catch
trang web tương tự .
Câu hỏi
Tôi phải đi với cờ lỗi nội bộ nếu tôi chọn để tránh ngoại lệ. Nó sẽ là quá nhiều bận tâm để xử lý, hoặc nó có thể sẽ làm việc thậm chí còn tốt hơn so với ngoại lệ? Một so sánh của cả hai trường hợp sẽ là câu trả lời tốt nhất.
Tránh các ngoại lệ hoàn toàn trong C ++ có vẻ cực kỳ phản tác dụng đối với tôi, trừ khi bạn làm việc trong một số hệ thống nhúng hoặc một loại trường hợp cụ thể cấm sử dụng chúng (trong trường hợp đó bạn cũng phải tránh ra để tránh tất cả thư viện và chức năng ngôn ngữ mà nếu không throw
, như sử dụng nghiêm ngặt nothrow
new
).
Nếu bạn hoàn toàn phải tránh các ngoại lệ vì bất kỳ lý do gì (ví dụ: hoạt động trên các ranh giới API C của mô-đun có API C mà bạn xuất), nhiều người có thể không đồng ý với tôi nhưng tôi thực sự khuyên bạn nên sử dụng trình xử lý lỗi toàn cầu như OpenGL glGetError()
. Bạn có thể làm cho nó sử dụng lưu trữ luồng cục bộ để có trạng thái lỗi duy nhất cho mỗi luồng.
Lý do của tôi cho điều đó là tôi không quen nhìn thấy các đội trong môi trường sản xuất kiểm tra kỹ tất cả các lỗi có thể xảy ra, thật không may, khi mã lỗi được trả về. Nếu chúng kỹ lưỡng, một số API C có thể gặp lỗi chỉ với mỗi cuộc gọi API C duy nhất và kiểm tra kỹ lưỡng sẽ yêu cầu một cái gì đó như:
if ((err = ApiCall(...)) != success)
{
// Handle error
}
... với hầu hết mọi dòng mã gọi API yêu cầu kiểm tra như vậy. Tuy nhiên, tôi đã không có may mắn làm việc với các đội kỹ lưỡng. Họ thường bỏ qua một nửa lỗi như vậy, đôi khi thậm chí là hầu hết thời gian. Đó là sự hấp dẫn lớn nhất đối với tôi về các ngoại lệ. Nếu chúng tôi bọc API này và làm cho nó đồng nhất throw
khi gặp lỗi, ngoại lệ có thể bị bỏ qua , và theo quan điểm của tôi, và kinh nghiệm, đó là điểm ưu việt của ngoại lệ.
Nhưng nếu các trường hợp ngoại lệ không thể được sử dụng, thì trạng thái lỗi trên mỗi luồng toàn cầu ít nhất có lợi thế (rất lớn so với việc trả lại mã lỗi cho tôi) rằng nó có thể gặp lỗi cũ muộn hơn một chút so với khi nó xảy ra xảy ra trong một số cơ sở mã hóa cẩu thả thay vì hoàn toàn thiếu nó và khiến chúng ta hoàn toàn không biết gì về những gì đã xảy ra. Lỗi có thể đã xảy ra một vài dòng trước đó hoặc trong một cuộc gọi chức năng trước đó, nhưng với điều kiện phần mềm chưa bị lỗi, chúng tôi có thể bắt đầu làm việc ngược lại và tìm ra nơi và lý do xảy ra.
Dường như với tôi rằng vì con trỏ rất hiếm, tôi phải sử dụng cờ lỗi nội bộ nếu tôi chọn tránh ngoại lệ.
Tôi không nhất thiết phải nói con trỏ là hiếm. Thậm chí có các phương thức trong C ++ 11 trở đi để có được các con trỏ dữ liệu cơ bản của các container và một nullptr
từ khóa mới . Nói chung, nó được coi là không khôn ngoan khi sử dụng các con trỏ thô để sở hữu / quản lý bộ nhớ nếu bạn có thể sử dụng một cái gì đó giống như unique_ptr
thay vào đó là mức độ quan trọng của việc tuân thủ RAII khi có ngoại lệ. Nhưng những con trỏ thô không sở hữu / quản lý bộ nhớ không nhất thiết bị coi là quá tệ (ngay cả từ những người như Sutter và Stroustrup) và đôi khi rất thực tế như một cách để chỉ vào mọi thứ (cùng với các chỉ số chỉ vào sự vật).
Chúng được cho là không kém an toàn so với các trình vòng lặp container tiêu chuẩn (ít nhất là trong bản phát hành, các trình vòng kiểm tra vắng mặt) sẽ không phát hiện ra nếu bạn cố gắng hủy đăng ký chúng sau khi chúng bị vô hiệu. C ++ vẫn còn là một ngôn ngữ nguy hiểm, tôi nói, trừ khi việc sử dụng cụ thể của nó muốn bao bọc mọi thứ và che giấu ngay cả những con trỏ thô không sở hữu. Điều này gần như rất quan trọng với các ngoại lệ rằng các tài nguyên tuân thủ RAII (thường không có chi phí thời gian chạy), nhưng khác với việc nó không nhất thiết phải là ngôn ngữ an toàn nhất để sử dụng để tránh chi phí mà nhà phát triển không muốn rõ ràng đổi lấy thứ khác Việc sử dụng được đề xuất không cố gắng bảo vệ bạn khỏi những thứ như con trỏ lơ lửng và các trình vòng lặp không hợp lệ, có thể nói (nếu không chúng tôi sẽ được khuyến khích sử dụngshared_ptr
khắp nơi, mà Stroustrup phản đối kịch liệt). Đó là cố gắng bảo vệ bạn khỏi thất bại trong việc giải phóng / giải phóng / phá hủy / mở khóa / dọn dẹp tài nguyên đúng cách khi có thứ gì đó throws
.