C ++ Iterator trọn đời và phát hiện mất hiệu lực


8

Dựa trên những gì được coi là thành ngữ trong C ++ 11:

  • một iterator vào một container tùy chỉnh tồn tại trong chính container bị phá hủy?
  • có thể phát hiện khi iterator bị vô hiệu?
  • các điều kiện trên về "bản dựng gỡ lỗi" trong thực tế?

Chi tiết : Gần đây tôi đã tìm hiểu về C ++ của mình và tìm hiểu về C ++ 11. Là một phần trong đó, tôi đã viết một trình bao bọc thành ngữ xung quanh thư viện uriparser . Một phần của điều này là gói đại diện danh sách liên kết của các thành phần đường dẫn được phân tích cú pháp. Tôi đang tìm kiếm lời khuyên về những gì thành ngữ cho container.

Một điều khiến tôi lo lắng, gần đây nhất là từ các ngôn ngữ được thu gom rác, là đảm bảo rằng các đối tượng ngẫu nhiên sẽ không biến mất trên người dùng nếu họ mắc lỗi liên quan đến tuổi thọ. Để giải thích cho điều này, cả PathListcontainer và các trình vòng lặp của nó đều giữ một shared_ptrđối tượng trạng thái bên trong thực tế. Điều này đảm bảo rằng miễn là mọi thứ trỏ vào dữ liệu đó tồn tại, dữ liệu cũng vậy.

Tuy nhiên, nhìn vào STL (và rất nhiều tìm kiếm), có vẻ như các thùng chứa C ++ đảm bảo điều này. Tôi có sự nghi ngờ khủng khiếp này rằng kỳ vọng là chỉ để các container bị phá hủy, vô hiệu hóa bất kỳ trình vòng lặp nào cùng với nó. std::vectorchắc chắn dường như để cho các trình vòng lặp có được chức năng không hợp lệ và vẫn (không chính xác).

Điều tôi muốn biết là: những gì được mong đợi từ mã C ++ 11 "tốt" / thành ngữ? Với các con trỏ thông minh mới sáng bóng, có vẻ lạ khi STL cho phép bạn dễ dàng thổi bay chân của mình bằng cách vô tình làm rò rỉ một iterator. Việc sử dụng shared_ptrdữ liệu sao lưu có phải là không hiệu quả không cần thiết, một ý tưởng tốt để gỡ lỗi hoặc điều gì đó được mong đợi mà STL không làm được?

(Tôi hy vọng rằng việc căn cứ điều này thành "thành ngữ C ++ 11" sẽ tránh được sự chủ quan ...)

Câu trả lời:


10

Việc sử dụng shared_ptrdữ liệu sao lưu không hiệu quả không cần thiết

Có - nó buộc phải có thêm một chỉ định và phân bổ thêm cho mỗi phần tử, và trong các chương trình đa luồng, mỗi lần tăng / giảm của số tham chiếu sẽ rất tốn kém ngay cả khi một container nhất định chỉ được sử dụng trong một luồng.

Tất cả những điều này có thể tốt, và thậm chí là mong muốn, trong một số trường hợp, nhưng quy tắc chung là không áp đặt các chi phí không cần thiết mà người dùng không thể tránh , ngay cả khi chúng vô dụng.

Kể từ khi không ai trong số những chi phí chung là cần thiết, nhưng thay vì gỡ lỗi niceties (và nhớ, không chính xác iterator đời là một lỗi tĩnh logic, không một số hành vi runtime lạ), không ai sẽ cảm ơn bạn đã chậm lại của họ đúng mã để bắt bạn lỗi.


Vì vậy, với câu hỏi ban đầu:

một iterator vào một container tùy chỉnh tồn tại trong chính container bị phá hủy?

câu hỏi thực sự là, liệu chi phí theo dõi tất cả các trình vòng lặp trực tiếp vào một container và vô hiệu hóa chúng khi container bị phá hủy, có được áp dụng cho những người có mã chính xác không?

Tôi nghĩ có lẽ là không, mặc dù nếu có một số trường hợp thực sự khó quản lý vòng đời của trình lặp một cách chính xác và bạn sẵn sàng thực hiện cú đánh, thì một thùng chứa chuyên dụng (hoặc bộ chuyển đổi container) cung cấp dịch vụ này có thể được thêm vào dưới dạng tùy chọn .

Ngoài ra, chuyển sang triển khai gỡ lỗi dựa trên cờ trình biên dịch có thể hợp lý, nhưng đó là một thay đổi lớn hơn và tốn kém hơn nhiều so với hầu hết được kiểm soát bởi DEBUG / NDEBUG. Đó chắc chắn là một thay đổi lớn hơn so với việc loại bỏ các câu lệnh khẳng định hoặc sử dụng bộ cấp phát gỡ lỗi.


Tôi đã quên đề cập đến, nhưng giải pháp sử dụng shared_ptrở mọi nơi của bạn không nhất thiết phải sửa lỗi của bạn: dù sao đi nữa, nó có thể chỉ trao đổi nó với một lỗi khác , cụ thể là rò rỉ bộ nhớ.


"chi phí của việc theo dõi tất cả các trình vòng lặp trực tiếp vào một container và vô hiệu hóa chúng khi container bị phá hủy, có được áp dụng cho những người có mã chính xác không?" quái không ở tất cả . Như bài đăng của bạn chỉ ra, một trong những phương châm thực tế của C ++ là "bạn không trả tiền cho những gì bạn không sử dụng". Đây là một lý do rất chính đáng: nó sẽ làm tê liệt nhiều dự án được lập trình tốt nếu họ phải kiểm tra ý thức chống lại tất cả những điều ngu ngốc mà một lập trình viên tồi có thể làm. Nhưng, tất nhiên, như bạn đã chỉ ra, nếu ai đó thực sự muốn điều đó ... họ có các công cụ để tự thực hiện (và giữ nó). Tốt nhất của cả hai thế giới!
gạch dưới

7

Trong C ++, nếu bạn để container bị hủy, thì các trình vòng lặp trở nên không hợp lệ. Ít nhất điều này có nghĩa là iterator là vô dụng, và nếu bạn cố gắng bỏ qua nó, thì rất nhiều điều tồi tệ có thể xảy ra (chính xác là nó tệ đến mức nào khi thực hiện, nhưng nó thường khá tệ).

Trong một ngôn ngữ như C ++, lập trình viên có trách nhiệm giữ những điều như vậy. Đó là một trong những thế mạnh của ngôn ngữ, bởi vì bạn có thể phụ thuộc khá nhiều vào thời điểm xảy ra (bạn đã xóa một đối tượng? Điều đó có nghĩa là tại thời điểm xóa, hàm hủy sẽ được gọi và bộ nhớ sẽ được giải phóng, và bạn có thể phụ thuộc trên đó), nhưng điều đó cũng có nghĩa là bạn không thể giữ các trình vòng lặp vào các thùng chứa ở khắp mọi nơi và sau đó xóa vùng chứa đó.

Bây giờ, bạn có thể viết một thùng chứa dữ liệu xung quanh cho đến khi hết các vòng lặp không? Tất nhiên, bạn rõ ràng đã có được điều đó. Đó không phải là cách C ++ thông thường, nhưng không có gì sai với nó, miễn là nó được ghi lại đúng cách (và tất nhiên, được gỡ lỗi). Đó không chỉ là cách các container STL hoạt động.


1
lưu ý rằng điều xấu có thể đi từ việc trả lại một tình cảm cho đến hành vi không xác định
ratchet freak

@ratchetfreak - vâng, điều đó đúng. Trong trường hợp được đề cập (iterators vào một thùng chứa) thường không có cách nào tốt để xác định giá trị cảm tính, vì vậy cách C ++ thông thường (và hành vi của STL) có xu hướng 'hành vi không xác định'.
Michael Kohne

5

Một trong những khác biệt (thường không nói) giữa các ngôn ngữ C ++ và GC là thành ngữ C ++ chính thống giả định rằng tất cả các lớp là các lớp giá trị.

Có con trỏ và tham chiếu, nhưng chúng chủ yếu là xuống hạng trong việc cho phép gửi đa hình (thông qua chức năng ảo) hoặc quản lý đối tượng có thời gian tồn tại của một trong những người tạo ra chúng.

Trong trường hợp cuối cùng này, trách nhiệm của lập trình viên là xác định chính sách và chính trị về ai tạo ra và ai và khi nào phải tiêu diệt. Con trỏ thông minh (như shared_ptrhoặc unique_ptr) chỉ là công cụ để trợ giúp trong nhiệm vụ này trong các trường hợp cụ thể (và thường xuyên) một đối tượng được "chia sẻ" bởi các chủ sở hữu khác nhau (và bạn muốn người cuối cùng phá hủy nó) hoặc cần phải di chuyển qua các bối cảnh luôn luôn có một bối cảnh duy nhất sở hữu nó.

Các bộ điều khiển, theo thiết kế, chỉ có ý nghĩa trong khi ... lặp lại, và do đó chúng không nên được "lưu trữ để sử dụng sau" vì những gì chúng đề cập đến, không được giữ nguyên hoặc giữ nguyên ở đó (một container có thể di chuyển nó nội dung khi tăng hoặc thu hẹp ... làm mất hiệu lực mọi thứ). Các thùng chứa dựa trên liên kết (như lists) là một ngoại lệ đối với quy tắc chung này, chứ không phải chính quy tắc này.

Trong C ++ thành ngữ nếu A "cần" B, B phải được sở hữu ở nơi tồn tại lâu hơn nơi sở hữu A, do đó không cần "theo dõi cuộc sống" của B từ A.

shared_ptrweak_ptrgiúp đỡ khi thành ngữ này quá hạn chế, bằng cách cho phép tương ứng "không biến mất cho đến khi tất cả chúng tôi cho phép bạn" hoặc chính sách "nếu bạn đi xa chỉ để lại tin nhắn cho chúng tôi". Nhưng họ có một chi phí, vì - để làm điều đó - họ phải phân bổ một số dữ liệu phụ trợ.

Bước tiếp theo là gc_ptr-s (thư viện chuẩn không cung cấp, nhưng bạn có thể thực hiện nếu muốn, sử dụng thuật toán quét ví dụ & đánh dấu) trong đó các cấu trúc theo dõi sẽ phức tạp hơn và chuyên sâu hơn về bộ xử lý bảo trì của họ.


4

Trong C ++, nó là thành ngữ để tạo ra bất cứ thứ gì

  • có thể được ngăn chặn bằng cách mã hóa cẩn thận và
  • sẽ phải chịu chi phí thời gian chạy để bảo vệ chống lại

một hành vi không xác định .

Trong trường hợp cụ thể của các trình vòng lặp, tài liệu của mỗi container cho biết các hoạt động nào làm mất hiệu lực các trình vòng lặp (việc phá hủy container luôn nằm trong số chúng) và việc truy cập vào trình vòng lặp không hợp lệ là Hành vi không xác định. Trong thực tế, điều đó có nghĩa là thời gian chạy sẽ truy cập một cách mù quáng con trỏ không còn hợp lệ. Thông thường nó gặp sự cố, nhưng nó có thể bị hỏng bộ nhớ và gây ra kết quả hoàn toàn không thể đoán trước.

Cung cấp các kiểm tra tùy chọn có thể được bật trong chế độ gỡ lỗi (với #definemặc định đó là bật nếu _DEBUGđược xác định và vô hiệu hóa nếu NDEBUGcó) là một cách làm tốt.

Tuy nhiên, hãy nhớ rằng C ++ được thiết kế để xử lý các trường hợp mà người ta cần mỗi bit hiệu năng và việc kiểm tra đôi khi có thể khá tốn kém, vì các trình vòng lặp thường được sử dụng trong các vòng lặp chặt chẽ, vì vậy đừng bật chúng theo mặc định.

Trong dự án công việc của chúng tôi, tôi đã phải vô hiệu hóa kiểm tra iterator trong thư viện tiêu chuẩn của Microsoft ngay cả trong chế độ gỡ lỗi, bởi vì một số container sử dụng các container và iterator khác trong nội bộ và chỉ cần phá hủy một cái lớn là mất nửa giờ vì kiểm tra!

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.