Việc sử dụng khẳng định () trong C ++ có xấu không?


92

Tôi có xu hướng thêm nhiều xác nhận vào mã C ++ của mình để giúp việc gỡ lỗi dễ dàng hơn mà không ảnh hưởng đến hiệu suất của các bản phát hành. Bây giờ, assertlà một macro C thuần túy được thiết kế mà không có cơ chế C ++.

Mặt khác std::logic_error, C ++ định nghĩa , có nghĩa là được ném trong trường hợp có lỗi trong logic của chương trình (do đó có tên). Việc ném một thể hiện có thể chỉ là sự thay thế hoàn hảo, nhiều hơn C ++ assert.

Vấn đề là assertabortcả hai đều chấm dứt chương trình ngay lập tức mà không gọi trình hủy, do đó bỏ qua việc dọn dẹp, trong khi việc ném một ngoại lệ theo cách thủ công sẽ làm tăng thêm chi phí thời gian chạy không cần thiết. Một cách giải quyết vấn đề này sẽ tạo ra một macro xác nhận của riêng mình SAFE_ASSERT, hoạt động giống như đối tác C, nhưng ném một ngoại lệ khi bị lỗi.

Tôi có thể nghĩ đến ba ý kiến ​​về vấn đề này:

  • Bám vào lời khẳng định của C. Vì chương trình bị chấm dứt ngay lập tức, không quan trọng liệu các thay đổi có được thực hiện chính xác hay không. Ngoài ra, sử dụng #defines trong C ++ cũng không tốt.
  • Ném một ngoại lệ và bắt nó trong hàm main () . Việc cho phép mã bỏ qua hàm hủy ở bất kỳ trạng thái nào của chương trình là một cách làm không tốt và phải tránh bằng mọi giá, và các lệnh gọi đến chấm dứt () cũng vậy. Nếu trường hợp ngoại lệ được ném ra, chúng phải được bắt.
  • Ném một ngoại lệ và để nó kết thúc chương trình. Một trường hợp ngoại lệ chấm dứt một chương trình là được và do đó NDEBUG, điều này sẽ không bao giờ xảy ra trong một bản phát hành. Việc nắm bắt là không cần thiết và tiết lộ chi tiết triển khai của mã nội bộ main().

Có câu trả lời chắc chắn cho vấn đề này không? Bất kỳ tài liệu tham khảo chuyên nghiệp?

Đã chỉnh sửa: Tất nhiên, bỏ qua hàm hủy không phải là hành vi không xác định.


22
Không, thực sự, logic_errorlà lỗi logic. Một lỗi trong logic của chương trình được gọi là lỗi. Bạn không giải quyết lỗi bằng cách ném các ngoại lệ.
R. Martinho Fernandes

4
Xác định, ngoại lệ, mã lỗi. Mỗi cái có một trường hợp sử dụng hoàn toàn riêng biệt và bạn không nên sử dụng cái này khi cần cái khác.
Kerrek SB

5
Đảm bảo rằng bạn sử dụng static_assertở nơi thích hợp nếu bạn có sẵn.
Flexo

4
@trion Tôi không thấy cách đó giúp ích gì. Bạn sẽ ném std::bug?
R. Martinho Fernandes

3
@trion: Đừng làm vậy. Ngoại lệ không dành cho gỡ lỗi. Ai đó có thể bắt được ngoại lệ. Không cần phải lo lắng về UB khi gọi std::abort(); nó sẽ chỉ đưa ra một tín hiệu khiến quá trình kết thúc.
Kerrek SB

Câu trả lời:


73

Các khẳng định hoàn toàn phù hợp trong mã C ++. Các ngoại lệ và các cơ chế xử lý lỗi khác không thực sự dành cho những điều tương tự như các xác nhận.

Xử lý lỗi dành cho khi có khả năng khôi phục hoặc thông báo lỗi độc đáo cho người dùng. Ví dụ: nếu có lỗi khi cố đọc tệp đầu vào, bạn có thể muốn thực hiện điều đó. Lỗi có thể là do lỗi, nhưng chúng cũng có thể đơn giản là đầu ra thích hợp cho đầu vào nhất định.

Các xác nhận dành cho những thứ như kiểm tra xem các yêu cầu của API có được đáp ứng khi API thường không được kiểm tra hay không hoặc để kiểm tra những thứ mà nhà phát triển tin rằng mình được đảm bảo bằng cách xây dựng. Ví dụ: nếu một thuật toán yêu cầu đầu vào được sắp xếp, bạn thường sẽ không kiểm tra điều đó, nhưng bạn có thể có một xác nhận để kiểm tra nó để gỡ lỗi các bản dựng gắn cờ loại lỗi đó. Một khẳng định luôn phải chỉ ra một chương trình hoạt động không chính xác.


Nếu bạn đang viết một chương trình mà việc tắt không sạch có thể gây ra sự cố thì bạn có thể muốn tránh các xác nhận. Hành vi không xác định đúng theo ngôn ngữ C ++ không đủ điều kiện là một vấn đề như vậy ở đây, vì việc nhấn một xác nhận có thể đã là kết quả của hành vi không xác định hoặc vi phạm một số yêu cầu khác có thể ngăn một số quá trình dọn dẹp hoạt động bình thường.

Ngoài ra, nếu bạn triển khai các xác nhận dưới dạng một ngoại lệ thì nó có thể bị bắt và 'xử lý' ngay cả khi điều này mâu thuẫn với chính mục đích của khẳng định.


1
Tôi không hoàn toàn chắc chắn liệu điều này có được nêu cụ thể trong câu trả lời hay không, vì vậy tôi sẽ nói rõ ở đây: bạn không nên sử dụng xác nhận cho bất kỳ điều gì liên quan đến đầu vào của người dùng mà không thể xác định được tại thời điểm viết mã. Nếu người dùng chuyển 3thay vì chuyển 1đến mã của bạn, nói chung, nó sẽ không kích hoạt xác nhận. Các xác nhận chỉ là lỗi của lập trình viên, không phải lỗi của người dùng thư viện hoặc ứng dụng.
SS Anne

101
  • Các xác nhận là để gỡ lỗi . Người dùng mã đã gửi của bạn sẽ không bao giờ nhìn thấy chúng. Nếu một xác nhận được nhấn, mã của bạn cần được sửa.

    CWE-617: Xác nhận có thể tiếp cận

Sản phẩm chứa một câu lệnh khẳng định () hoặc tương tự có thể được kích hoạt bởi kẻ tấn công, dẫn đến việc thoát ứng dụng hoặc hành vi khác nghiêm trọng hơn mức cần thiết.

Mặc dù xác nhận là tốt để bắt lỗi logic và giảm cơ hội tiếp cận các điều kiện lỗ hổng nghiêm trọng hơn, nhưng nó vẫn có thể dẫn đến từ chối dịch vụ.

Ví dụ: nếu máy chủ xử lý nhiều kết nối đồng thời và một xác nhận () xảy ra trong một kết nối duy nhất khiến tất cả các kết nối khác bị ngắt, thì đây là một xác nhận có thể truy cập được dẫn đến từ chối dịch vụ.

  • Các trường hợp ngoại lệ dành cho những trường hợp ngoại lệ . Nếu gặp phải, người dùng sẽ không thể làm những gì mình muốn nhưng có thể tiếp tục ở một nơi khác.

  • Xử lý lỗi dành cho luồng chương trình bình thường. Ví dụ: nếu bạn nhắc người dùng nhập một số và nhận được nội dung nào đó không thể phân tích được, điều đó là bình thường , vì thông tin người dùng nhập không nằm trong tầm kiểm soát của bạn và tất nhiên bạn phải luôn xử lý tất cả các tình huống có thể xảy ra. (Ví dụ: lặp lại cho đến khi bạn có đầu vào hợp lệ, ở giữa là "Xin lỗi, thử lại".)


1
đã tìm kiếm điều này khẳng định lại; bất kỳ hình thức xác nhận nào đi qua mã sản xuất đều dẫn đến thiết kế và chất lượng kém. Điểm mà một xác nhận được gọi là nơi nên xử lý một cách dễ dàng tình trạng lỗi. (Tôi không bao giờ sử dụng khẳng định). Đối với các trường hợp ngoại lệ, trường hợp sử dụng duy nhất mà tôi biết là khi ctor có thể bị lỗi, tất cả các trường hợp khác là để xử lý lỗi thông thường.
slashmais

5
@slashmais: Tình cảm này rất đáng khen ngợi, nhưng trừ khi bạn đang gửi mã hoàn hảo, không có lỗi, tôi thấy một khẳng định (thậm chí là một hành vi gây sự cố cho người dùng) thích hợp hơn hành vi không xác định. Lỗi xảy ra trong các hệ thống phức tạp và chắc chắn rằng bạn có cách nhìn và chẩn đoán nó xảy ra ở đâu.
Kerrek SB

@KerrekSB Tôi muốn sử dụng một ngoại lệ hơn một khẳng định. Ít nhất thì mã có cơ hội loại bỏ nhánh bị lỗi và làm điều gì đó hữu ích. Ít nhất, nếu bạn đang sử dụng RAII, tất cả bộ đệm của bạn để mở tệp sẽ được xả đúng cách.
daemonspring

14

Các xác nhận có thể được sử dụng để xác minh các bất biến thực thi bên trong, như trạng thái bên trong trước hoặc sau khi thực thi một số phương thức, v.v. Nếu xác nhận không thành công, điều đó thực sự có nghĩa là logic của chương trình bị hỏng và bạn không thể khôi phục điều này. Trong trường hợp này, tốt nhất bạn có thể làm là phá vỡ càng sớm càng tốt mà không chuyển ngoại lệ cho người dùng. Điều thực sự hay về các xác nhận (ít nhất là trên Linux) là kết xuất lõi được tạo ra do kết thúc quá trình và do đó bạn có thể dễ dàng điều tra dấu vết ngăn xếp và các biến. Điều này hữu ích hơn nhiều để hiểu lỗi logic hơn là thông báo ngoại lệ.


Tôi có một cách tiếp cận tương tự. Tôi sử dụng các xác nhận cho logic có lẽ phải đúng cục bộ (ví dụ: bất biến vòng lặp). Các trường hợp ngoại lệ dành cho trường hợp lỗi logic bị buộc đối với mã bởi một tình huống phi địa phương (bên ngoài).
spff

Nếu một xác nhận không thành công, điều đó có nghĩa là logic của một phần chương trình bị hỏng. Một khẳng định thất bại không nhất thiết ngụ ý rằng không thể hoàn thành được điều . Một plugin bị hỏng có lẽ không nên hủy bỏ toàn bộ trình xử lý văn bản.
daemonspring 29/08/17

13

Không chạy hàm hủy do alling abort () không phải là hành vi không xác định!

Nếu đúng như vậy, thì đó cũng sẽ là hành vi không xác định để gọi std::terminate(), và vì vậy việc cung cấp nó sẽ là gì?

assert() cũng hữu ích trong C ++ như trong C. Các câu lệnh không phải để xử lý lỗi, chúng dùng để hủy bỏ chương trình ngay lập tức.


1
Tôi muốn nói abort()là hủy bỏ chương trình ngay lập tức. Bạn đúng khi xác nhận không phải để xử lý lỗi, tuy nhiên, xác nhận cố gắng xử lý lỗi bằng cách hủy bỏ. Thay vào đó, bạn không nên ném một ngoại lệ và để người gọi xử lý lỗi nếu có thể? Rốt cuộc, người gọi ở một vị trí tốt hơn để xác định xem lỗi của một chức năng có khiến nó không đáng phải làm bất cứ điều gì khác hay không. Có thể người gọi đang cố gắng thực hiện ba công việc không liên quan và vẫn có thể hoàn thành hai công việc khác và chỉ cần loại bỏ công việc này.
daemonspring 29/08/17

assertđược định nghĩa để gọi abort(khi điều kiện sai). Đối với việc ném các ngoại lệ, không, điều đó không phải lúc nào cũng thích hợp. Người gọi không thể xử lý một số việc. Người gọi không thể xác định xem lỗi logic trong chức năng thư viện của bên thứ ba có thể khôi phục được hay dữ liệu bị hỏng có thể được sửa hay không.
Jonathan Wakely

6

IMHO, các xác nhận là để kiểm tra các điều kiện mà nếu vi phạm, mọi thứ khác trở nên vô nghĩa. Và do đó bạn không thể phục hồi từ chúng hay đúng hơn là khôi phục là không liên quan.

Tôi sẽ nhóm chúng thành 2 loại:

  • Tội lỗi của nhà phát triển (ví dụ: hàm xác suất trả về giá trị âm):

xác suất float () {return -1.0; }

khẳng định (xác suất ()> = 0.0)

  • Máy bị hỏng (ví dụ: máy chạy chương trình của bạn rất sai):

int x = 1;

khẳng định (x> 0);

Đây đều là những ví dụ tầm thường nhưng không quá xa rời thực tế. Ví dụ: hãy nghĩ về các thuật toán ngây thơ trả về chỉ số âm khi sử dụng với vectơ. Hoặc các chương trình nhúng trong phần cứng tùy chỉnh. Hay đúng hơn là vì sh * t xảy ra .

Và nếu có những lỗi phát triển như vậy, bạn không nên tự tin về bất kỳ cơ chế khôi phục hoặc xử lý lỗi nào được thực hiện. Điều tương tự cũng áp dụng cho các lỗi phần cứng.


1
khẳng định (xác suất ()> = 0.0)
Elliott
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.