Có hợp lý để bảo vệ null mỗi con trỏ bị hủy bỏ không?


65

Ở một công việc mới, tôi đã bị đánh dấu trong các đánh giá mã cho mã như thế này:

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender) { }

void PowerManager::SignalShutdown()
{
    msgSender_->sendMsg("shutdown()");
}

Tôi đã nói rằng phương pháp cuối cùng nên đọc:

void PowerManager::SignalShutdown()
{
    if (msgSender_) {
        msgSender_->sendMsg("shutdown()");
    }
}

tức là, tôi phải đặt một NULLngười bảo vệ xung quanh msgSender_biến, mặc dù đó là một thành viên dữ liệu riêng tư. Thật khó cho tôi để kiềm chế bản thân khỏi việc sử dụng thám hiểm để mô tả cảm giác của tôi về phần 'trí tuệ' này. Khi tôi yêu cầu một lời giải thích, tôi nhận được một loạt các câu chuyện kinh dị về cách một số lập trình viên cơ sở, một số năm, đã bối rối về cách một lớp học phải làm việc và vô tình xóa một thành viên mà anh ta không nên có (và đặt nó NULLsau đó , rõ ràng), và mọi thứ đã nổ tung trên cánh đồng ngay sau khi phát hành sản phẩm và chúng tôi đã "học một cách khó khăn, tin tưởng chúng tôi" rằng tốt hơn là chỉ cần NULLkiểm tra mọi thứ .

Đối với tôi, điều này cảm thấy giống như lập trình sùng bái hàng hóa , đơn giản và đơn giản. Một vài đồng nghiệp tốt bụng đang cố gắng giúp tôi 'lấy nó' và xem điều này sẽ giúp tôi viết mã mạnh mẽ hơn như thế nào, nhưng ... tôi không thể cảm thấy như họ là những người không hiểu được .

Có hợp lý không khi một tiêu chuẩn mã hóa yêu cầu mọi con trỏ đơn lẻ được kiểm duyệt trong một hàm phải được kiểm tra cho NULLcác thành viên dữ liệu riêng tư đầu tiên ngay cả? (Lưu ý: Để đưa ra một số bối cảnh, chúng tôi tạo ra một thiết bị điện tử tiêu dùng, không phải là hệ thống kiểm soát không lưu hoặc một số sản phẩm 'thất bại tương đương với người chết'.)

EDIT : Trong ví dụ trên, msgSender_cộng tác viên không phải là tùy chọn. Nếu nó đã từng NULL, nó chỉ ra một lỗi. Lý do duy nhất nó được truyền vào hàm tạo là PowerManagercó thể được kiểm tra với một IMsgSenderlớp con giả .

TÓM TẮT : Có một số câu trả lời thực sự tuyệt vời cho câu hỏi này, cảm ơn tất cả mọi người. Tôi đã chấp nhận một từ @aaronps chủ yếu do tính ngắn gọn của nó. Dường như có một thỏa thuận chung khá rộng rằng:

  1. Bảo NULLvệ bắt buộc cho mọi con trỏ bị hủy bỏ đơn lẻ là quá mức cần thiết, nhưng
  2. Bạn có thể bước bên cạnh toàn bộ cuộc tranh luận bằng cách sử dụng một tham chiếu thay thế (nếu có thể) hoặc một constcon trỏ và
  3. assertbáo cáo là một thay thế được khai sáng hơn cho các NULLvệ sĩ để xác minh rằng các điều kiện tiên quyết của chức năng được đáp ứng.

14
Đừng nghĩ trong một phút rằng sai lầm là lãnh địa của các lập trình viên cơ sở. 95% các nhà phát triển của tất cả các cấp độ kinh nghiệm sẽ làm điều gì đó một lần trong một thời gian. 5% còn lại đang nói dối qua răng của họ.
Blrfl

56
Nếu tôi thấy ai đó viết mã kiểm tra null và âm thầm thất bại, tôi muốn bắn họ ngay tại chỗ.
Winston Ewert

16
Thứ thất bại âm thầm là khá nhiều điều tồi tệ nhất. Ít nhất là khi nó nổ tung bạn biết nơi vụ nổ xảy ra. Kiểm tra nullvà không làm gì chỉ là một cách để di chuyển lỗi xuống thực thi, khiến việc theo dõi lại nguồn trở nên khó khăn hơn nhiều.
MirroredFate

1
+1, câu hỏi rất thú vị. Ngoài câu trả lời của tôi, tôi cũng chỉ ra rằng khi bạn phải viết mã, mục đích của bạn là để kiểm tra đơn vị, điều bạn thực sự làm là giao dịch tăng rủi ro cho cảm giác an toàn sai lầm (nhiều dòng mã hơn = nhiều khả năng cho lỗi). Thiết kế lại để sử dụng tài liệu tham khảo không chỉ cải thiện khả năng đọc mã, mà còn giảm số lượng mã (và kiểm tra) mà bạn phải viết để chứng minh rằng nó hoạt động.
Seth

6
@Rig, tôi chắc chắn rằng có những trường hợp thích hợp cho một thất bại thầm lặng. Nhưng nếu ai đó im lặng thất bại trong thực tiễn chung (trừ khi làm việc trong một lĩnh vực có ý nghĩa), tôi thực sự không muốn thực hiện dự án mà họ đang đặt mã.
Winston Ewert

Câu trả lời:


66

Nó phụ thuộc vào 'hợp đồng':

Nếu PowerManager PHẢI có giá trị IMsgSender, không bao giờ kiểm tra null, hãy để nó chết sớm hơn.

Mặt khác, nó CÓ THỂ có một IMsgSender, sau đó bạn cần kiểm tra mỗi khi bạn sử dụng, đơn giản như vậy.

Nhận xét cuối cùng về câu chuyện của lập trình viên cơ sở, vấn đề thực sự là thiếu quy trình kiểm tra.


4
+1 cho một câu trả lời rất ngắn gọn mà gần như tổng hợp theo cách tôi cảm nhận: "nó phụ thuộc ..." Bạn cũng có can đảm nói "không bao giờ kiểm tra null" ( nếu null không hợp lệ). Đặt một assert()chức năng trong mọi thành viên để hủy bỏ một con trỏ là một sự thỏa hiệp có thể sẽ xoa dịu rất nhiều người, nhưng thậm chí điều đó có cảm giác như 'hoang tưởng đầu cơ' đối với tôi. Danh sách những thứ vượt quá khả năng kiểm soát của tôi là vô hạn, và một khi tôi cho phép bản thân bắt đầu lo lắng về chúng, nó sẽ trở thành một con dốc thực sự trơn trượt. Học cách tin tưởng vào các bài kiểm tra của mình, đồng nghiệp của tôi và người sửa lỗi của tôi đã giúp tôi ngủ ngon hơn vào ban đêm.
evadeflow

2
Khi bạn đọc mã, các xác nhận đó có thể rất hữu ích trong việc hiểu cách mã được cho là hoạt động. Tôi chưa bao giờ trang bị một codebase khiến tôi phải đi 'Tôi ước nó không có tất cả những khẳng định này ở khắp mọi nơi.', Nhưng tôi đã thấy rất nhiều nơi tôi nói ngược lại.
Kristof Provost

1
Đơn giản và đơn giản, đây là câu trả lời chính xác cho câu hỏi.
Matthew Azkimov

2
Đây là câu trả lời đúng gần nhất nhưng tôi không thể đồng ý với 'không bao giờ kiểm tra null', luôn kiểm tra các tham số không hợp lệ tại điểm chúng sẽ được gán cho một biến thành viên.
James

Cảm ơn các ý kiến. Tốt hơn là để nó sập sớm hơn nếu ai đó không đọc thông số kỹ thuật và khởi tạo nó với giá trị null khi nó phải có một cái gì đó hợp lệ ở đó.
aaronps

77

Tôi cảm thấy mã nên đọc:

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender)
{
    assert(msgSender);
}

void PowerManager::SignalShutdown()
{
    assert(msgSender_);
    msgSender_->sendMsg("shutdown()");
}

Điều này thực sự tốt hơn so với việc bảo vệ NULL, bởi vì nó cho thấy rất rõ rằng hàm không bao giờ được gọi nếu msgSender_là NULL. Nó cũng đảm bảo rằng bạn sẽ chú ý nếu điều này xảy ra.

"Sự khôn ngoan" được chia sẻ của các đồng nghiệp của bạn sẽ âm thầm bỏ qua lỗi này, với kết quả không thể đoán trước.

Nói chung, các lỗi dễ sửa hơn nếu chúng được phát hiện gần hơn với nguyên nhân của chúng. Trong ví dụ này, bộ bảo vệ NULL được đề xuất sẽ dẫn đến một thông báo tắt máy không được thiết lập, điều này có thể hoặc không thể dẫn đến một lỗi đáng chú ý. Bạn sẽ gặp khó khăn hơn khi làm việc ngược với SignalShutdownchức năng so với khi toàn bộ ứng dụng vừa chết, tạo ra một backtrace tiện lợi hoặc kết xuất lõi chỉ trực tiếp vào SignalShutdown().

Đó là một chút phản trực giác, nhưng sụp đổ ngay khi có bất cứ điều gì sai có xu hướng làm cho mã của bạn mạnh mẽ hơn. Đó là bởi vì bạn thực sự tìm thấy các vấn đề, và cũng có xu hướng có những nguyên nhân rất rõ ràng.


10
Sự khác biệt lớn giữa việc gọi xác nhận và ví dụ mã của OP là xác nhận đó chỉ được bật trong các bản dựng gỡ lỗi (#define NDEBUG) Khi sản phẩm vào tay khách hàng và gỡ lỗi bị vô hiệu hóa và một đường dẫn mã không bao giờ được thực thi sẽ rời khỏi con trỏ null mặc dù chức năng trong câu hỏi được thực thi, khẳng định không cung cấp cho bạn sự bảo vệ nào.
atk

17
Có lẽ bạn đã thử nghiệm bản dựng trước khi thực sự gửi nó cho khách hàng, nhưng đúng vậy, đây là một rủi ro có thể xảy ra. Mặc dù vậy, các giải pháp rất dễ dàng: chỉ cần vận chuyển với các xác nhận được bật (dù sao đó cũng là phiên bản bạn đã kiểm tra) hoặc thay thế bằng macro của riêng bạn, xác nhận trong chế độ gỡ lỗi và chỉ ghi nhật ký, nhật ký + backtraces, thoát, ... trong chế độ phát hành.
Kristof Provost

7
@James - trong 90% trường hợp bạn sẽ không phục hồi từ kiểm tra NULL không thành công. Nhưng trong trường hợp như vậy, mã sẽ âm thầm thất bại và lỗi có thể xuất hiện nhiều sau này - ví dụ bạn nghĩ rằng bạn đã lưu vào tệp nhưng thực tế kiểm tra null đôi khi không thành công khiến bạn không khôn ngoan hơn. Bạn không thể phục hồi từ tất cả các tình huống không được xảy ra. Bạn có thể xác minh rằng chúng sẽ không xảy ra, nhưng tôi không nghĩ bạn có ngân sách trừ khi bạn viết mã cho nhà máy điện vệ tinh hoặc hạt nhân của NASA. Bạn cần phải có cách nói "Tôi không biết chuyện gì đang xảy ra - chương trình không giả sử ở trạng thái này".
Maciej Piechotka

6
@James tôi không đồng ý với việc ném. Điều đó làm cho nó trông giống như một cái gì đó thực sự có thể xảy ra. Khẳng định là cho những thứ được cho là không thể. Đối với các lỗi thực sự trong mã nói cách khác. Ngoại lệ là khi bất ngờ, nhưng có thể, mọi thứ xảy ra. Ngoại lệ là cho những thứ bạn có thể hiểu được xử lý và phục hồi từ. Lỗi logic trong mã bạn không thể phục hồi và bạn cũng không nên thử.
Kristof Provost

2
@James: Theo kinh nghiệm của tôi với các hệ thống nhúng, phần mềm và phần cứng luôn được thiết kế sao cho cực kỳ khó để khiến một thiết bị hoàn toàn không sử dụng được. Trong phần lớn các trường hợp, có một bộ giám sát phần cứng buộc thiết lập lại hoàn toàn thiết bị nếu phần mềm ngừng chạy.
Bart van Ingen Schenau

30

Nếu Trình thông báo không bao giờ phải như vậy null, bạn chỉ nên đặt nullkiểm tra trong hàm tạo. Điều này cũng đúng với bất kỳ đầu vào nào khác vào lớp - đặt kiểm tra tính toàn vẹn tại điểm nhập vào 'mô-đun' - lớp, chức năng, v.v.

Nguyên tắc nhỏ của tôi là thực hiện kiểm tra tính toàn vẹn giữa các ranh giới mô-đun - lớp trong trường hợp này. Ngoài ra, một lớp phải đủ nhỏ để có thể nhanh chóng xác minh tinh thần về tính toàn vẹn của thời gian sống của các thành viên trong lớp - đảm bảo rằng các lỗi như xóa không đúng / bài tập null được ngăn chặn. Kiểm tra null được thực hiện trong mã trong bài đăng của bạn giả định rằng bất kỳ việc sử dụng không hợp lệ nào thực sự gán null cho con trỏ - điều này không phải lúc nào cũng đúng. Vì 'sử dụng không hợp lệ' vốn ngụ ý rằng bất kỳ giả định nào về mã thông thường không được áp dụng, chúng tôi không thể chắc chắn bắt được tất cả các loại lỗi con trỏ - ví dụ: xóa, tăng không hợp lệ, v.v.

Ngoài ra - nếu bạn chắc chắn đối số không bao giờ có thể là null, hãy xem xét sử dụng tài liệu tham khảo, tùy thuộc vào cách sử dụng lớp của bạn. Mặt khác, xem xét sử dụng std::unique_ptrhoặc std::shared_ptrthay cho một con trỏ thô.


11

Không, không hợp lý để kiểm tra sự khác biệt của con trỏ đối với con trỏ NULL.

Kiểm tra con trỏ Null có giá trị đối với các đối số hàm (bao gồm cả đối số hàm tạo) để đảm bảo các điều kiện tiên quyết được đáp ứng hoặc thực hiện hành động thích hợp nếu không cung cấp tham số tùy chọn và chúng có giá trị để kiểm tra bất biến của lớp sau khi bạn đã hiển thị phần bên trong của lớp. Nhưng nếu lý do duy nhất để một con trỏ trở thành NULL là sự tồn tại của một lỗi, thì không có điểm nào trong việc kiểm tra. Lỗi đó có thể dễ dàng đặt con trỏ đến một giá trị không hợp lệ khác.

Nếu tôi phải đối mặt với một tình huống như của bạn, tôi sẽ hỏi hai câu hỏi:

  • Tại sao lỗi từ lập trình viên cơ sở đó không bị bắt trước khi phát hành? Ví dụ trong một đánh giá mã hoặc trong giai đoạn thử nghiệm? Đưa một thiết bị điện tử tiêu dùng bị lỗi ra thị trường có thể tốn kém (nếu bạn chiếm thị phần và thiện chí) khi phát hành một thiết bị bị lỗi được sử dụng trong một ứng dụng quan trọng về an toàn, vì vậy tôi hy vọng công ty sẽ nghiêm túc trong việc thử nghiệm và các hoạt động QA khác.
  • Nếu kiểm tra null thất bại, bạn sẽ xử lý lỗi nào để xử lý lỗi, hoặc tôi chỉ có thể viết assert(msgSender_)thay vì kiểm tra null? Nếu bạn chỉ đặt kiểm tra null, bạn có thể đã ngăn được sự cố, nhưng bạn có thể đã tạo ra một tình huống tồi tệ hơn vì phần mềm tiếp tục với tiền đề là một hoạt động đã diễn ra trong khi thực tế hoạt động đó bị bỏ qua. Điều này có thể dẫn đến các phần khác của phần mềm trở nên không ổn định.

9

Ví dụ này dường như liên quan nhiều đến tuổi thọ của đối tượng hơn là liệu tham số đầu vào có null hay không. Vì bạn đề cập rằng PowerManagerphải luôn có một giá trị hợp lệ IMsgSender, việc truyền đối số bằng con trỏ (do đó cho phép khả năng của một con trỏ null) tấn công tôi như một lỗ hổng thiết kế.

Trong các tình huống như thế này, tôi muốn thay đổi giao diện để các yêu cầu của người gọi được thực thi bằng ngôn ngữ:

PowerManager::PowerManager(const IMsgSender& msgSender)
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    msgSender_->sendMsg("shutdown()");
}

Viết lại theo cách này nói rằng PowerManagercần phải giữ một tài liệu tham khảo IMsgSendercho toàn bộ cuộc đời của nó. Điều này, đến lượt nó, cũng thiết lập một yêu cầu ngụ ý IMsgSenderphải sống lâu hơn PowerManagervà phủ nhận sự cần thiết cho bất kỳ kiểm tra hoặc xác nhận con trỏ null bên trong PowerManager.

Bạn cũng có thể viết điều tương tự bằng cách sử dụng một con trỏ thông minh (thông qua boost hoặc c ++ 11), để rõ ràng buộc IMsgSenderphải sống lâu hơn PowerManager:

PowerManager::PowerManager(std::shared_ptr<IMsgSender> msgSender) 
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    // Here, we own a smart pointer to IMsgSender, so even if the caller
    // destroys the original pointer, we still have a valid copy
    msgSender_->sendMsg("shutdown()");
}

Phương pháp này được ưa thích nếu có thể IMsgSenderlà thời gian tồn tại không thể được đảm bảo dài hơn so với PowerManager(nghĩa là x = new IMsgSender(); p = new PowerManager(*x);).

Liên quan đến con trỏ: kiểm tra null tràn lan làm cho mã khó đọc hơn và không cải thiện tính ổn định (nó cải thiện sự xuất hiện của tính ổn định, điều này tệ hơn nhiều).

Ở đâu đó, ai đó có một địa chỉ cho bộ nhớ để giữ IMsgSender. Trách nhiệm của chức năng đó là đảm bảo rằng việc phân bổ đã thành công (kiểm tra các giá trị trả về của thư viện hoặc xử lý các std::bad_allocngoại lệ đúng cách ), để không vượt qua các con trỏ không hợp lệ.

PowerManagerkhông sở hữu IMsgSender(nó chỉ mượn nó trong một thời gian), nên nó không chịu trách nhiệm phân bổ hoặc phá hủy bộ nhớ đó. Đây là một lý do tại sao tôi thích tham khảo.

Vì bạn là người mới trong công việc này, tôi hy vọng rằng bạn đang hack mã hiện có. Vì vậy, do lỗi thiết kế, ý tôi là lỗ hổng đó nằm trong mã mà bạn đang làm việc. Do đó, những người đang gắn cờ mã của bạn bởi vì nó không kiểm tra các con trỏ null thực sự đang tự gắn cờ để viết mã yêu cầu con trỏ :)


1
Thực tế không có gì sai khi sử dụng con trỏ thô đôi khi. Std :: shared_ptr không phải là viên đạn bạc và sử dụng nó trong danh sách đối số là một phương pháp chữa trị cho kỹ thuật cẩu thả mặc dù tôi nhận ra rằng nó được coi là 'leet' để sử dụng nó bất cứ khi nào có thể. Sử dụng java nếu bạn cảm thấy như vậy.
James

2
Tôi không nói con trỏ thô là xấu! Họ chắc chắn có vị trí của họ. Tuy nhiên, đây là C + + , các tham chiếu (và các con trỏ được chia sẻ, bây giờ) là một phần của ngôn ngữ vì một lý do. Sử dụng chúng, chúng sẽ khiến bạn thành công.
Seth

Tôi thích ý tưởng con trỏ thông minh, nhưng nó không phải là một lựa chọn trong buổi biểu diễn đặc biệt này. +1 để đề xuất sử dụng tài liệu tham khảo (như @Max và một vài người khác có). Tuy nhiên, đôi khi không thể tiêm tham chiếu, do các vấn đề phụ thuộc gà và trứng, tức là, nếu một đối tượng có thể là ứng cử viên cho tiêm cần phải có một con trỏ (hoặc tham chiếu) cho chủ sở hữu của nó được tiêm vào . Điều này đôi khi có thể chỉ ra một thiết kế kết hợp chặt chẽ; nhưng nó thường được chấp nhận, như trong mối quan hệ giữa một đối tượng và trình vòng lặp của nó, nơi nó không bao giờ có ý nghĩa để sử dụng cái sau mà không có cái trước.
evadeflow

8

Giống như các trường hợp ngoại lệ, các điều kiện bảo vệ chỉ hữu ích nếu bạn biết phải làm gì để khắc phục lỗi hoặc nếu bạn muốn đưa ra một thông báo ngoại lệ có ý nghĩa hơn.

Nuốt lỗi (cho dù bị bắt là ngoại lệ hoặc kiểm tra bảo vệ), chỉ là điều đúng đắn khi lỗi không thành vấn đề. Nơi phổ biến nhất để tôi thấy lỗi bị nuốt là trong mã đăng nhập lỗi - bạn không muốn làm hỏng ứng dụng vì bạn không thể đăng nhập thông báo trạng thái.

Bất cứ khi nào một chức năng được gọi và đó không phải là hành vi tùy chọn, nó sẽ thất bại lớn tiếng, không âm thầm.

Chỉnh sửa: khi nghĩ về câu chuyện lập trình viên cơ sở của bạn, có vẻ như những gì đã xảy ra là một thành viên tư nhân được đặt thành null khi điều đó không bao giờ được phép xảy ra. Họ gặp vấn đề với văn bản không phù hợp và đang cố gắng khắc phục bằng cách xác nhận khi đọc. Điều này là ngược. Tại thời điểm bạn xác định nó, lỗi đã xảy ra. Giải pháp xem xét mã / trình biên dịch mã cho các điều kiện không bảo vệ, mà thay vào đó là getters và setters hoặc const thành viên.


7

Như những người khác đã lưu ý, điều này phụ thuộc vào việc msgSendercó thể hợp pháp hay không NULL. Sau đây giả định rằng nó không bao giờ nên là NULL.

void PowerManager::SignalShutdown()
{
    if (!msgSender_)
    {
       throw SignalException("Shut down failed because message sender is not set.");
    }

    msgSender_->sendMsg("shutdown()");
}

Đề xuất "sửa chữa" của những người khác trong nhóm của bạn vi phạm nguyên tắc Dead Programs Tell No Lies . Lỗi thực sự rất khó tìm. Một phương pháp âm thầm thay đổi hành vi của nó dựa trên một vấn đề trước đó, không chỉ khiến bạn khó tìm ra lỗi đầu tiên mà còn thêm lỗi thứ 2 của chính nó.

Các thiếu niên tàn phá bằng cách không kiểm tra null. Điều gì sẽ xảy ra nếu đoạn mã này tàn phá bằng cách tiếp tục chạy trong trạng thái không xác định (thiết bị đang bật nhưng chương trình "nghĩ" nó bị tắt)? Có lẽ một phần khác của chương trình sẽ làm một cái gì đó chỉ an toàn khi thiết bị tắt.

Một trong những cách tiếp cận này sẽ tránh được những thất bại thầm lặng:

  1. Sử dụng các xác nhận theo đề xuất của câu trả lời này , nhưng đảm bảo rằng chúng được bật trong mã sản xuất. Điều này, tất nhiên, có thể gây ra vấn đề nếu các khẳng định khác được viết với giả định rằng chúng sẽ bị ngừng sản xuất.

  2. Ném một ngoại lệ nếu nó là null.


5

Tôi đồng ý với bẫy null trong hàm tạo. Hơn nữa, nếu thành viên được khai báo trong tiêu đề là:

IMsgSender* const msgSender_;

Sau đó, con trỏ không thể thay đổi sau khi khởi tạo, vì vậy nếu nó ổn khi xây dựng, nó sẽ ổn trong suốt vòng đời của đối tượng chứa nó. (Đối tượng được trỏ đến sẽ không phải là const.)


Đó là lời khuyên tuyệt vời , cảm ơn bạn! Thật sự rất tốt, trên thực tế, tôi xin lỗi tôi không thể bình chọn nó cao hơn nữa. Đây không phải là câu trả lời trực tiếp cho câu hỏi đã nêu, nhưng đó là một cách tuyệt vời để loại bỏ mọi khả năng con trỏ 'vô tình' trở thành NULL.
evadeflow

@evadeflow Tôi nghĩ rằng "Tôi đồng ý với những người khác" đã trả lời lực đẩy chính của câu hỏi. Chúc mừng dù! ;-)
Grimm The Opiner

Nó sẽ là một chút bất lịch sự của tôi để thay đổi câu trả lời được chấp nhận tại thời điểm này, cho cách câu hỏi được đặt ra. Nhưng tôi thực sự nghĩ rằng lời khuyên này là nổi bật và tôi xin lỗi tôi đã không nghĩ về nó. Với một constcon trỏ, không cần assert()bên ngoài hàm tạo, vì vậy câu trả lời này có vẻ tốt hơn một chút so với @Kristof Provost (cũng rất nổi bật, nhưng cũng giải quyết câu hỏi một cách gián tiếp.) Tôi hy vọng những người khác sẽ bỏ phiếu này, vì nó thực sự không có ý nghĩa asserttrong mọi phương thức khi bạn chỉ có thể tạo con trỏ const.
evadeflow

@evadeflow Mọi người chỉ truy cập Questins cho đại diện, bây giờ nó đã được trả lời không ai sẽ nhìn lại. ;-) Tôi chỉ xuất hiện vì đó là một câu hỏi về con trỏ và tôi đang để mắt đến lữ đoàn "Sử dụng con trỏ thông minh cho mọi thứ luôn luôn".
Grimm The Opiner

3

Điều này là hoàn toàn nguy hiểm!

Tôi đã làm việc dưới một nhà phát triển cao cấp trong một cơ sở mã C với "tiêu chuẩn" tồi tệ nhất, người đã thúc đẩy điều tương tự, để kiểm tra một cách mù quáng tất cả các con trỏ cho null. Nhà phát triển cuối cùng sẽ làm những việc như thế này:

// Pre: vertex should never be null.
void transform_vertex(Vertex* vertex, ...)
{
    // Inserted by my "wise" co-worker.
    if (!vertex)
        return;
    ...
}

Tôi đã từng thử loại bỏ kiểm tra điều kiện tiên quyết một lần trong một chức năng như vậy và thay thế nó bằng một assertđể xem điều gì sẽ xảy ra.

Điều kinh hoàng của tôi, tôi đã tìm thấy hàng ngàn dòng mã trong cơ sở mã đã chuyển null thành hàm này, nhưng ở đó các nhà phát triển, có thể nhầm lẫn, đã làm việc xung quanh và chỉ thêm mã cho đến khi mọi thứ hoạt động.

Điều kinh khủng hơn nữa của tôi, tôi thấy vấn đề này là phổ biến ở tất cả các nơi trong quá trình kiểm tra cơ sở mã cho null. Các codebase đã phát triển trong nhiều thập kỷ để dựa vào các kiểm tra này để có thể âm thầm vi phạm ngay cả các điều kiện tiên quyết được ghi chép rõ ràng nhất. Bằng cách loại bỏ các kiểm tra chết người này theo hướng có lợi asserts, tất cả các lỗi logic của con người trong nhiều thập kỷ trong cơ sở mã sẽ được tiết lộ và chúng tôi sẽ nhấn chìm chúng.

Chỉ mất hai dòng mã dường như vô hại như thế này + thời gian và một nhóm để kết thúc việc che giấu hàng ngàn lỗi tích lũy.

Đây là những loại thực hành làm cho các lỗi phụ thuộc vào các lỗi khác tồn tại để phần mềm hoạt động. Đó là một kịch bản ác mộng . Nó cũng làm cho mọi lỗi logic liên quan đến việc vi phạm các điều kiện tiên quyết như vậy xuất hiện một cách bí ẩn hàng triệu dòng mã khỏi trang web thực sự xảy ra lỗi, vì tất cả các kiểm tra null này chỉ ẩn lỗi và ẩn lỗi cho đến khi chúng tôi đến một nơi mà quên để che giấu lỗi.

Đối với tôi, chỉ đơn giản là kiểm tra null một cách mù quáng ở mọi nơi mà con trỏ null vi phạm điều kiện tiên quyết, đối với tôi, sự điên rồ hoàn toàn, trừ khi phần mềm của bạn rất quan trọng đối với các thất bại khẳng định và sự cố sản xuất mà tiềm năng của kịch bản này là thích hợp hơn.

Có hợp lý không khi một tiêu chuẩn mã hóa yêu cầu mọi con trỏ đơn lẻ được quy định trong một hàm phải được kiểm tra cho NULL đầu tiên ngay cả các thành viên dữ liệu riêng tư?

Vì vậy, tôi muốn nói, hoàn toàn không. Nó thậm chí không "an toàn". Nó rất có thể ngược lại và che giấu tất cả các loại lỗi trong toàn bộ cơ sở mã của bạn, qua nhiều năm, có thể dẫn đến các kịch bản khủng khiếp nhất.

assertlà cách để đi đến đây Vi phạm các điều kiện tiên quyết không được phép không được chú ý, nếu không luật pháp của Murphy có thể dễ dàng bị phạt.


1
"khẳng định" là cách để đi - nhưng hãy nhớ rằng trên bất kỳ hệ thống hiện đại nào, mỗi lần truy cập bộ nhớ thông qua một con trỏ đều có một con trỏ null xác nhận được tích hợp vào phần cứng. Vì vậy, trong "assert (p! = NULL); return * p;", ngay cả khẳng định đó hầu như là vô nghĩa.
gnasher729

@ gnasher729 Ah, đó là một điểm rất tốt, nhưng tôi có xu hướng xem assertcả hai cơ chế tài liệu để thiết lập các điều kiện tiên quyết và không chỉ là một cách để buộc phải hủy bỏ. Nhưng tôi cũng giống như bản chất của lỗi xác nhận cho bạn biết chính xác dòng mã nào gây ra lỗi và điều kiện khẳng định không thành công ("ghi lại sự cố"). Có thể có ích nếu chúng ta kết thúc việc sử dụng bản dựng gỡ lỗi bên ngoài trình gỡ lỗi và bị mất cảnh giác.

@ gnasher729 Hoặc tôi có thể đã hiểu nhầm một phần của nó. Các hệ thống hiện đại này có hiển thị dòng mã nguồn nào trong đó xác nhận thất bại không? Tôi thường tưởng tượng rằng họ đã mất thông tin vào thời điểm đó và có thể chỉ hiển thị vi phạm truy cập / vi phạm truy cập, nhưng tôi hơi xa "hiện đại" - vẫn ngoan cố trên Windows 7 trong phần lớn sự phát triển của tôi.

Ví dụ: trên MacOS X và iOS, nếu ứng dụng của bạn gặp sự cố do truy cập con trỏ null trong quá trình phát triển, trình gỡ lỗi sẽ cho bạn biết dòng mã chính xác nơi xảy ra. Có một khía cạnh kỳ lạ khác: Trình biên dịch sẽ cố gắng đưa ra cảnh báo nếu bạn làm điều gì đó đáng ngờ. Nếu bạn chuyển một con trỏ tới một hàm và truy cập vào nó, trình biên dịch sẽ không đưa ra cảnh báo rằng con trỏ đó có thể là NULL, vì nó xảy ra quá thường xuyên, bạn sẽ bị chìm trong các cảnh báo. Tuy nhiên, nếu bạn thực hiện so sánh if (p == NULL) ... thì trình biên dịch giả định rằng có khả năng p là NULL,
gnasher729

bởi vì tại sao bạn lại kiểm tra nó, vì vậy nó sẽ đưa ra cảnh báo từ đó nếu bạn sử dụng nó mà không kiểm tra. Nếu bạn sử dụng macro giống như khẳng định của riêng mình, bạn phải khéo léo viết nó theo cách sao cho "my_assert (p! = NULL," Đó là một con trỏ rỗng, ngu ngốc! "); * P = 1;" không đưa ra cảnh báo cho bạn.
gnasher729

2

Ví dụ, Objective-C xử lý mọi lệnh gọi phương thức trên một nilđối tượng dưới dạng no-op để đánh giá giá trị zero-ish. Có một số lợi thế cho quyết định thiết kế đó trong Objective-C, vì những lý do được đề xuất trong câu hỏi của bạn. Khái niệm lý thuyết về bảo vệ null mỗi cuộc gọi phương thức có một số giá trị nếu nó được công bố rộng rãi và được áp dụng nhất quán.

Điều đó nói rằng, mã sống trong một hệ sinh thái, không phải là chân không. Hành vi được bảo vệ null sẽ không thành ngữ và đáng ngạc nhiên trong C ++, và do đó nên được coi là có hại. Tóm lại, không ai viết mã C ++ theo cách đó, vì vậy đừng làm điều đó! Như một phản ví dụ, tuy nhiên, lưu ý rằng gọi free()hoặc deletetrên một NULLtrong C và C ++ là đảm bảo được một không-op.


Trong ví dụ của bạn, có thể đáng để đặt một xác nhận trong hàm tạo msgSenderkhông phải là null. Nếu các nhà xây dựng được gọi là phương pháp trên msgSenderngay lập tức, sau đó không khẳng định như vậy sẽ là cần thiết, vì nó sẽ sụp đổ ngay tại đó anyway. Tuy nhiên, vì nó chỉ đơn thuần là lưu trữ msgSender để sử dụng trong tương lai, nên việc xem xét dấu vết của SignalShutdown()giá trị sẽ trở nên rõ ràng như thế nào NULL, do đó, một khẳng định trong hàm tạo sẽ giúp việc gỡ lỗi dễ dàng hơn.

Thậm chí tốt hơn, các nhà xây dựng nên chấp nhận một const IMsgSender&tham chiếu, mà không thể có thể NULL.


1

Lý do mà bạn được yêu cầu tránh các cuộc hội thảo null là để đảm bảo rằng mã của bạn mạnh mẽ. Ví dụ về các lập trình viên cơ sở từ lâu chỉ là ví dụ. Bất cứ ai cũng có thể phá vỡ mã một cách tình cờ và gây ra sự vô hiệu hóa - đặc biệt là đối với toàn cầu và toàn cầu. Trong C và C ++, nó thậm chí còn có thể vô tình hơn, với khả năng quản lý bộ nhớ trực tiếp. Bạn có thể ngạc nhiên, nhưng điều này xảy ra rất thường xuyên. Ngay cả bởi các nhà phát triển rất am hiểu, rất có kinh nghiệm và rất cao cấp.

Bạn không cần phải kiểm tra mọi thứ, nhưng bạn cần phải bảo vệ chống lại các cuộc hội thảo có khả năng là vô hiệu. Điều này thường xảy ra khi chúng được phân bổ, sử dụng và hủy đăng ký trong các chức năng khác nhau. Có thể một trong những chức năng khác sẽ bị sửa đổi và phá vỡ chức năng của bạn. Cũng có thể là một trong các chức năng khác có thể được gọi không theo thứ tự (như nếu bạn có một bộ xử lý có thể được gọi tách biệt với hàm hủy).

Tôi thích cách tiếp cận đồng nghiệp của bạn đang nói với bạn kết hợp với việc sử dụng khẳng định. Sự cố trong môi trường thử nghiệm vì vậy rõ ràng hơn là có một vấn đề cần khắc phục và thất bại trong sản xuất.

Bạn cũng nên sử dụng một công cụ sửa lỗi mã mạnh mẽ như độ che phủ hoặc củng cố. Và bạn nên giải quyết tất cả các cảnh báo trình biên dịch.

Chỉnh sửa: như những người khác đã đề cập, thất bại âm thầm, như trong một số ví dụ về mã, nói chung là điều sai. Nếu chức năng của bạn không thể phục hồi từ giá trị là null, nó sẽ trả về lỗi (hoặc ném ngoại lệ) cho người gọi. Người gọi có trách nhiệm sau đó sửa lỗi lệnh gọi, khôi phục hoặc trả lại lỗi (hoặc ném ngoại lệ) cho người gọi, v.v. Cuối cùng, một chức năng có thể phục hồi và di chuyển một cách duyên dáng, phục hồi và thất bại một cách duyên dáng (chẳng hạn như cơ sở dữ liệu bị lỗi giao dịch do lỗi nội bộ cho một người dùng nhưng không thoát ra được) hoặc chức năng xác định rằng trạng thái ứng dụng bị hỏng và không thể phục hồi và thoát ứng dụng.


+1 vì có can đảm để hỗ trợ một vị trí (dường như) không phổ biến. Tôi đã thất vọng nếu không ai bảo vệ đồng nghiệp của mình về điều này (họ là những người rất thông minh, người đã đưa ra một sản phẩm chất lượng trong nhiều năm). Tôi cũng thích ý tưởng rằng các ứng dụng sẽ "sụp đổ trong môi trường thử nghiệm ... và thất bại trong sản xuất." Tôi không tin rằng bắt buộc NULLkiểm tra 'vẹt' là cách để làm điều đó, nhưng tôi đánh giá cao việc nghe ý kiến ​​từ một người nào đó bên ngoài nơi làm việc của tôi, người về cơ bản nói: "Aigh. Có vẻ không quá vô lý."
evadeflow

Tôi cảm thấy khó chịu khi thấy mọi người thử nghiệm NULL sau malloc (). Nó cho tôi biết họ không hiểu hệ điều hành hiện đại quản lý bộ nhớ như thế nào.
Kristof Provost

2
Tôi đoán là @KristofProvost đang nói về các hệ điều hành sử dụng quá mức, mà malloc luôn thành công (nhưng sau đó HĐH có thể giết quá trình của bạn nếu thực sự không có đủ bộ nhớ). Tuy nhiên, đây không phải là lý do chính đáng để bỏ qua kiểm tra null trên malloc: overcommit không phổ biến trên các nền tảng và ngay cả khi nó được bật, có thể có những lý do khác khiến malloc thất bại (ví dụ: trên Linux, giới hạn không gian địa chỉ quy trình được đặt bằng ulimit ).
John Bartholomew

1
John nói gì. Tôi đã thực sự nói về overcommit. Overcommit được bật theo mặc định trên Linux (đây là khoản thanh toán hóa đơn của tôi). Tôi không biết, nhưng nghi ngờ như vậy, nếu nó giống nhau trên Windows. Trong hầu hết các trường hợp, dù sao bạn cũng sẽ gặp sự cố, trừ khi bạn chuẩn bị viết rất nhiều mã chưa từng có, sẽ được kiểm tra để xử lý các lỗi gần như chắc chắn sẽ không bao giờ xảy ra ...
Kristof Provost

2
Tôi xin nói thêm rằng tôi cũng chưa bao giờ thấy mã (bên ngoài nhân linux, nơi hết bộ nhớ là có thể thực tế) sẽ thực sự xử lý chính xác tình trạng hết bộ nhớ. Tất cả các mã mà tôi đã thấy thử sẽ bị sập sau đó. Tất cả những gì đạt được là lãng phí thời gian, làm cho mã khó hiểu hơn và che giấu vấn đề thực sự. Nere dereferences rất dễ gỡ lỗi (nếu bạn có một tệp cốt lõi).
Kristof Provost

1

Việc sử dụng một con trỏ thay vì một tài liệu tham khảo sẽ nói với tôi rằng msgSenderchỉ là tùy chọn và đây việc kiểm tra null sẽ là đúng đắn. Đoạn mã quá chậm để quyết định điều đó. Có thể có những yếu tố khác PowerManagercó giá trị (hoặc có thể kiểm chứng) ...

Khi chọn giữa con trỏ và tham chiếu, tôi cân nhắc kỹ lưỡng cả hai tùy chọn. Nếu tôi phải sử dụng một con trỏ cho một thành viên (ngay cả đối với các thành viên tư nhân), tôi phải chấp nhận if (x)thủ tục mỗi lần tôi hủy bỏ nó.


Như tôi đã đề cập trong một bình luận khác ở đâu đó, có những lúc việc tiêm một tài liệu tham khảo là không thể. Một ví dụ tôi gặp rất nhiều là bản thân đối tượng bị giữ cần tham chiếu đến chủ sở hữu:
evadeflow

@evadeflow: OK, tôi thú nhận, nó phụ thuộc vào quy mô lớp học và khả năng hiển thị của các thành viên con trỏ (không thể tạo tài liệu tham khảo), cho dù tôi có kiểm tra trước khi hội thảo. Trong trường hợp lớp học nhỏ và đơn giản và (các) con trỏ là riêng tư, tôi không ...
Wolf

0

Nó phụ thuộc vào những gì bạn muốn xảy ra.

Bạn đã viết rằng bạn đang làm việc trên "một thiết bị điện tử tiêu dùng". Nếu, bằng cách nào đó, một lỗi được giới thiệu bằng cách đặt msgSender_thành NULL, bạn có muốn

  • thiết bị để tiếp tục, bỏ qua SignalShutdownnhưng tiếp tục phần còn lại của hoạt động, hoặc
  • Thiết bị gặp sự cố, buộc người dùng phải khởi động lại?

Tùy thuộc vào tác động của tín hiệu tắt máy không được gửi, tùy chọn 1 có thể là lựa chọn khả thi. Nếu người dùng có thể tiếp tục nghe nhạc của mình, nhưng màn hình vẫn hiển thị tiêu đề của bản nhạc trước đó, điều đó thể thích hợp hơn cho sự cố hoàn toàn của thiết bị.

Tất nhiên, nếu bạn chọn tùy chọn 1, một assert(như được đề xuất bởi người khác) là rất quan trọng để giảm khả năng xảy ra lỗi như vậy khi không được chú ý trong quá trình phát triển. Bảo ifvệ null chỉ ở đó để giảm thiểu thất bại trong sử dụng sản xuất.

Cá nhân, tôi cũng thích cách tiếp cận "sự cố sớm" cho các bản dựng sản xuất, nhưng tôi đang phát triển phần mềm kinh doanh có thể sửa và cập nhật dễ dàng trong trường hợp có lỗi. Đối với các thiết bị điện tử tiêu dùng, điều này có thể không dễ dàng như vậy.

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.