std :: shared_ptr như là phương sách cuối cùng?


59

Tôi chỉ xem các luồng "Đi bản địa 2012" và tôi nhận thấy các cuộc thảo luận về std::shared_ptr. Tôi hơi ngạc nhiên khi nghe quan điểm hơi tiêu cực của Bjarne std::shared_ptrvà nhận xét của anh ta rằng nó nên được sử dụng như một "phương sách cuối cùng" khi thời gian sống của một đối tượng không chắc chắn (theo tôi, theo anh ta, không thường xuyên như vậy).

Bất cứ ai sẽ quan tâm để giải thích điều này sâu hơn một chút? Làm thế nào chúng ta có thể lập trình mà không std::shared_ptrvà vẫn quản lý thời gian sống của đối tượng một cách an toàn ?


8
Không sử dụng con trỏ? Có một chủ sở hữu riêng biệt của đối tượng, mà quản lý suốt đời?
Bo Persson

2
những gì về dữ liệu được chia sẻ rõ ràng? Thật khó để không sử dụng con trỏ. Ngoài ra std :: shared_pulum sẽ làm "quản lý trọn đời" bẩn thỉu trong trường hợp đó
Kamil Klimek

6
Bạn đã xem xét lắng nghe ít hơn những lời khuyên được trình bày và nhiều hơn cho các tranh luận đằng sau lời khuyên đó? Ông giải thích khá tốt về loại hệ thống mà loại lời khuyên này sẽ hoạt động.
Nicol Bolas

@NicolBolas: Tôi lắng nghe lời khuyên và lập luận nhưng rõ ràng tôi không cảm thấy mình hiểu nó đủ rõ.
ronag

Tại thời điểm nào anh ấy nói "phương sách cuối cùng"? Theo dõi bit tại 36 phút trong ( channel9.msdn.com/Events/GoingNative/GoingNative-2012/ Khăn ) anh ấy nói rằng anh ấy cảnh giác với việc sử dụng con trỏ, nhưng anh ấy có nghĩa là con trỏ nói chung, không chỉ chia sẻ_ptr và unique_ptr mà thậm chí ' con trỏ 'thông thường. Ông ngụ ý rằng các đối tượng tự (và không phải con trỏ đến các đối tượng được phân bổ mới) nên được ưu tiên. Là bit bạn đã nghĩ về sau trong bài thuyết trình?
Pharap

Câu trả lời:


55

Nếu bạn có thể tránh quyền sở hữu chung thì ứng dụng của bạn sẽ đơn giản và dễ hiểu hơn và do đó ít bị lỗi hơn trong quá trình bảo trì. Các mô hình sở hữu phức tạp hoặc không rõ ràng có xu hướng dẫn đến khó theo dõi các khớp nối của các phần khác nhau của ứng dụng thông qua trạng thái chia sẻ có thể không dễ theo dõi.

Vì điều này, tốt hơn là sử dụng các đối tượng có thời gian lưu trữ tự động và có các đối tượng phụ "giá trị". Thất bại điều này, unique_ptrcó thể là một sự thay thế tốt với shared_ptrviệc - nếu không phải là phương sách cuối cùng - một cách nào đó xuống danh sách các công cụ mong muốn.


5
+1 cho biết rằng vấn đề không phải là bản thân kỹ thuật (quyền sở hữu chung), mà là những khó khăn mà nó gây ra cho chúng ta chỉ là những người sau đó phải giải mã những gì đang diễn ra.
Matthieu M.

Tuy nhiên, thực hiện một cách tiếp cận như vậy sẽ hạn chế nghiêm trọng khả năng của lập trình viên trong việc áp dụng các mẫu lập trình đồng thời trên hầu hết các lớp OOP không tầm thường (do không thể sao chép.) Vấn đề này được nêu ra trong "Đi bản địa 2013".
rwong

48

Thế giới mà Bjarne sống trong đó rất ... hàn lâm, vì muốn có một thuật ngữ tốt hơn. Nếu mã của bạn có thể được thiết kế và cấu trúc sao cho các đối tượng có hệ thống phân cấp quan hệ rất có chủ ý, thì các mối quan hệ sở hữu là cứng nhắc và không chịu được, mã sẽ chảy theo một hướng (từ cấp cao đến cấp thấp) và các đối tượng chỉ nói chuyện với những người thấp hơn hệ thống phân cấp, sau đó bạn sẽ không tìm thấy nhiều nhu cầu shared_ptr. Đó là thứ bạn sử dụng trong những dịp hiếm hoi mà ai đó phải phá vỡ quy tắc. Nhưng nếu không, bạn chỉ có thể gắn mọi thứ trong vectors hoặc các cấu trúc dữ liệu khác sử dụng ngữ nghĩa giá trị và unique_ptrs cho những thứ bạn phải phân bổ đơn lẻ.

Mặc dù đó là một thế giới tuyệt vời để sống, nhưng đó không phải là điều bạn có thể làm mọi lúc. Nếu bạn không thể tổ chức mã của mình theo cách đó, bởi vì thiết kế hệ thống mà bạn đang cố gắng thực hiện có nghĩa là không thể (hoặc chỉ rất khó chịu), thì bạn sẽ thấy mình cần sở hữu chung các đối tượng ngày càng nhiều .

Trong một hệ thống như vậy, việc giữ các con trỏ trần trụi ... không chính xác là nguy hiểm, nhưng nó đặt ra câu hỏi. Điều tuyệt vời shared_ptrlà nó cung cấp các đảm bảo cú pháp hợp lý về tuổi thọ của đối tượng. Nó có thể bị phá vỡ? Tất nhiên. Nhưng mọi người cũng có thể const_castmọi thứ; chăm sóc cơ bản và cho ăn shared_ptrphải cung cấp chất lượng cuộc sống hợp lý cho các đối tượng được phân bổ, quyền sở hữu phải được chia sẻ.

Sau đó, có weak_ptrs, không thể được sử dụng trong trường hợp không có a shared_ptr. Nếu hệ thống của bạn có cấu trúc cứng nhắc, thì bạn có thể lưu trữ một con trỏ trần cho một đối tượng nào đó, an toàn với kiến ​​thức rằng cấu trúc của ứng dụng đảm bảo rằng đối tượng được chỉ ra sẽ tồn tại lâu hơn bạn. Bạn có thể gọi một hàm trả về một con trỏ tới một giá trị bên trong hoặc bên ngoài (ví dụ tìm đối tượng có tên X). Trong mã được cấu trúc đúng, chức năng đó sẽ chỉ khả dụng cho bạn nếu tuổi thọ của đối tượng được đảm bảo vượt quá chính bạn; do đó, lưu trữ con trỏ trần trong đối tượng của bạn là tốt.

Vì sự cứng nhắc đó không phải lúc nào cũng có thể đạt được trong các hệ thống thực, bạn cần một số cách để đảm bảo hợp lý trọn đời. Đôi khi, bạn không cần quyền sở hữu đầy đủ; đôi khi, bạn chỉ cần có thể biết khi nào con trỏ xấu hay tốt. Đó là nơi weak_ptrxuất hiện. Đã có trường hợp tôi có thể sử dụng unique_ptrhoặc boost::scoped_ptr, nhưng tôi phải sử dụng shared_ptrvì tôi đặc biệt cần thiết để đưa cho ai đó một con trỏ "dễ bay hơi". Một con trỏ mà cả đời không xác định được và chúng có thể truy vấn khi con trỏ đó bị phá hủy.

Một cách an toàn để tồn tại khi tình trạng của thế giới là không xác định.

Có thể điều đó đã được thực hiện bởi một số lệnh gọi hàm để lấy con trỏ, thay vì thông qua weak_ptr? Có, nhưng điều đó có thể dễ dàng bị phá vỡ hơn. Một hàm trả về một con trỏ trần không có cách nào về mặt cú pháp gợi ý rằng người dùng không làm điều gì đó giống như lưu trữ con trỏ đó lâu dài. Trả lại một cái shared_ptrcũng khiến cho ai đó dễ dàng lưu trữ nó và có khả năng kéo dài tuổi thọ của một vật thể. weak_ptrTuy nhiên, việc trả lại một gợi ý mạnh mẽ rằng lưu trữ những shared_ptrgì bạn nhận được locklà một ... ý tưởng đáng ngờ. Nó sẽ không ngăn bạn làm điều đó, nhưng không có gì trong C ++ ngăn bạn phá mã. weak_ptrcung cấp một số sức đề kháng tối thiểu từ việc làm điều tự nhiên.

Bây giờ, điều đó không có nghĩa là shared_ptrkhông thể sử dụng quá mức ; nó chắc chắn có thể Đặc biệt là trước unique_ptrđó, có nhiều trường hợp tôi chỉ sử dụng một boost::shared_ptrvì tôi cần phải vượt qua một con trỏ RAII xung quanh hoặc đưa nó vào một danh sách. Không có ngữ nghĩa di chuyển và unique_ptr, boost::shared_ptrlà giải pháp thực sự duy nhất.

Và bạn có thể sử dụng nó ở những nơi khá cần thiết. Như đã nêu ở trên, cấu trúc mã thích hợp có thể loại bỏ nhu cầu sử dụng một số shared_ptr. Nhưng nếu hệ thống của bạn không thể được cấu trúc như vậy và vẫn làm những gì nó cần, shared_ptrsẽ được sử dụng đáng kể.


4
+1: Xem ví dụ: boost :: asio. Tôi nghĩ rằng ý tưởng mở rộng ra nhiều lĩnh vực, bạn có thể không biết tại thời điểm biên dịch tiện ích UI hoặc cuộc gọi không đồng bộ nào là lần cuối để từ bỏ một đối tượng và với shared_ptr bạn không cần biết. Nó rõ ràng không áp dụng cho mọi tình huống, chỉ là một công cụ (rất hữu ích) trong hộp công cụ.
Guy Sirton

3
Một chút bình luận muộn màng; shared_ptrlà tuyệt vời cho các hệ thống mà c ++ tích hợp với ngôn ngữ script như python. Sử dụng boost::python, tham chiếu đếm trên c ++ và python hợp tác rất lớn; bất kỳ đối tượng nào từ c ++ vẫn có thể bị giữ trong python và nó sẽ không chết.
eudoxos

1
Chỉ để tham khảo, sự hiểu biết của tôi là không sử dụng WebKit hay Chromium shared_ptr. Cả hai sử dụng thực hiện riêng của họ intrusive_ptr. Tôi chỉ đưa ra điều đó bởi vì cả hai đều là ví dụ thực tế của các ứng dụng lớn được viết bằng C ++
gman

1
@gman: Tôi thấy nhận xét của bạn rất sai lệch, vì sự phản đối của Stroustrup được shared_ptráp dụng như nhau đối với intrusive_ptr: anh ấy phản đối toàn bộ khái niệm sở hữu chung, không theo bất kỳ cách viết cụ thể nào của khái niệm này. Vì vậy, mục đích của câu hỏi này, đó là hai ví dụ thực tế của các ứng dụng lớn mà làm sử dụng shared_ptr. (Và, những gì nhiều hơn, họ chứng minh rằng shared_ptrnó hữu ích ngay cả khi nó không kích hoạt weak_ptr.)
ruakh

1
FWIW, để chống lại tuyên bố rằng Bjarne đang sống trong thế giới học thuật: trong tất cả sự nghiệp công nghiệp thuần túy của tôi (bao gồm đồng kiến ​​trúc một sàn giao dịch chứng khoán G20 và chỉ kiến ​​trúc một MOG 500K người chơi) Tôi chỉ thấy 3 trường hợp khi chúng tôi thực sự cần sở hữu chung. Tôi là 200% với Bjarne ở đây.
No-Bugs Hare

37

Tôi không tin rằng tôi đã từng sử dụng std::shared_ptr.

Hầu hết thời gian, một đối tượng được liên kết với một số bộ sưu tập, mà nó thuộc về toàn bộ cuộc đời của nó. Trong trường hợp đó bạn chỉ có thể sử dụng whatever_collection<o_type>hoặc whatever_collection<std::unique_ptr<o_type>>, bộ sưu tập đó là thành viên của một đối tượng hoặc một biến tự động. Tất nhiên, nếu bạn không cần số lượng đối tượng động, bạn chỉ có thể sử dụng một mảng tự động có kích thước cố định.

Không lặp lại thông qua bộ sưu tập hoặc bất kỳ hoạt động nào khác trên đối tượng yêu cầu chức năng trợ giúp để chia sẻ quyền sở hữu ... nó sử dụng đối tượng, sau đó trả về và người gọi đảm bảo rằng đối tượng vẫn tồn tại cho toàn bộ cuộc gọi . Đây là hợp đồng được sử dụng nhiều nhất giữa người gọi và callee.


Nicol Bolas nhận xét rằng "Nếu một số đối tượng giữ một con trỏ trần trụi và đối tượng đó chết ... rất tiếc." và "Các đối tượng cần đảm bảo rằng đối tượng sống qua cuộc sống của đối tượng đó. Chỉ shared_ptrcó thể làm điều đó."

Tôi không mua lập luận đó. Ít nhất là không shared_ptrgiải quyết được vấn đề này. Thế còn:

  • Nếu một số bảng băm giữ một đối tượng và mã băm của đối tượng đó thay đổi ... rất tiếc.
  • Nếu một số hàm đang lặp lại một vectơ và một phần tử được chèn vào vectơ đó ... ôi.

Giống như bộ sưu tập rác, việc sử dụng mặc định shared_ptrkhuyến khích lập trình viên không nghĩ về hợp đồng giữa các đối tượng hoặc giữa chức năng và người gọi. Suy nghĩ về các điều kiện tiên quyết và hậu điều kiện chính xác là cần thiết, và tuổi thọ đối tượng chỉ là một phần nhỏ của chiếc bánh lớn hơn đó.

Các đối tượng không "chết", một số đoạn mã phá hủy chúng. Và ném shared_ptrvào vấn đề thay vì tìm ra hợp đồng cuộc gọi là một sự an toàn sai lầm.


17
@ronag: Tôi nghi ngờ rằng bạn đã bắt đầu sử dụng nó trong đó một con trỏ thô sẽ tốt hơn, bởi vì "con trỏ thô là xấu". Nhưng con trỏ thô không xấu . Chỉ tạo con trỏ đầu tiên, sở hữu một đối tượng thành một con trỏ thô là xấu, bởi vì sau đó bạn phải quản lý bộ nhớ theo cách thủ công, điều này không tầm thường khi có ngoại lệ. Nhưng sử dụng con trỏ thô làm tay cầm hoặc lặp là tốt.
Ben Voigt

4
@BenVoigt: Tất nhiên, khó khăn khi vượt qua các con trỏ trần trụi là bạn không biết tuổi thọ của các vật thể. Nếu một số đối tượng giữ một con trỏ trần và đối tượng đó chết ... rất tiếc. Đó chính xác là loại điều shared_ptrweak_ptrđược thiết kế để tránh. Bjarne cố gắng sống trong một thế giới là mọi thứ đều có một cuộc sống tốt đẹp, rõ ràng và mọi thứ được xây dựng xung quanh đó. Và nếu bạn có thể xây dựng thế giới đó, thật tuyệt. Nhưng đó không phải là thế giới thực. Các đối tượng cần đảm bảo rằng đối tượng sống qua cuộc sống của đối tượng đó. Chỉ shared_ptrcó thể làm điều đó.
Nicol Bolas

5
@NicolBolas: Đó là sự an toàn sai lầm. Nếu người gọi chức năng không cung cấp bảo đảm thông thường: "Đối tượng này sẽ không bị bất kỳ bên ngoài nào chạm vào trong khi gọi chức năng" thì cả hai cần phải đồng ý về loại sửa đổi bên ngoài nào được cho phép. shared_ptrchỉ giảm nhẹ một sửa đổi bên ngoài cụ thể, và thậm chí không phải là sửa đổi phổ biến nhất. Và đó không phải là trách nhiệm của đối tượng để đảm bảo tuổi thọ của nó là chính xác, nếu hợp đồng gọi hàm chỉ định khác.
Ben Voigt

6
@NicolBolas: Nếu một hàm tạo một đối tượng và trả về nó bằng con trỏ, thì nó phải là một unique_ptrbiểu thức rằng chỉ có một con trỏ tới đối tượng tồn tại và nó có quyền sở hữu.
Ben Voigt

6
@Nicol: Nếu nó đang tìm kiếm một con trỏ trong bộ sưu tập nào đó, có lẽ nên sử dụng bất kỳ loại con trỏ nào trong bộ sưu tập đó hoặc một con trỏ thô nếu bộ sưu tập giữ các giá trị. Nếu nó đang tạo một đối tượng và người gọi muốn a shared_ptr, thì nó vẫn trả về a unique_ptr. Chuyển đổi từ unique_ptrthành shared_ptrdễ dàng, nhưng điều ngược lại là không thể.
Ben Voigt

16

Tôi không thích suy nghĩ một cách tuyệt đối (như "phương sách cuối cùng") mà liên quan đến miền vấn đề.

C ++ có thể cung cấp một số cách khác nhau để quản lý trọn đời. Một số người trong số họ cố gắng dẫn lại các đối tượng theo cách ngăn xếp. Một số khác cố gắng để thoát khỏi giới hạn này. Một số trong số họ là "nghĩa đen", một số khác là gần đúng.

Thật ra bạn có thể:

  1. sử dụng ngữ nghĩa giá trị thuần túy . Hoạt động cho các đối tượng tương đối nhỏ trong đó điều quan trọng là "giá trị" chứ không phải "danh tính", trong đó bạn có thể giả sử rằng hai người Personcó cùng namemột người (tốt hơn: hai đại diện của cùng một người ). Thời gian tồn tại được cấp bởi ngăn xếp máy, cuối cùng - không quan trọng đối với chương trình (vì một ngườitên của nó , bất kể điều gì Personđang mang nó)
  2. sử dụng các đối tượng được phân bổ ngăn xếp và các tham chiếu hoặc con trỏ liên quan: cho phép đa hình và cấp tuổi thọ cho đối tượng. Không cần "con trỏ thông minh", vì bạn đảm bảo không có đối tượng nào có thể được "chỉ" bởi các cấu trúc để lại trong ngăn xếp dài hơn đối tượng mà chúng trỏ tới (đầu tiên tạo đối tượng, sau đó là các cấu trúc tham chiếu đến nó).
  3. sử dụng các đối tượng được phân bổ heap stack stack : đây là những gì std :: vector và tất cả các container làm, và wat std::unique_ptrlàm (bạn có thể nghĩ nó như một vector với kích thước 1). Một lần nữa, bạn thừa nhận đối tượng bắt đầu tồn tại (và kết thúc sự tồn tại của chúng) trước (sau) cấu trúc dữ liệu mà chúng đề cập đến.

Điểm yếu của mehtod này là các loại và số lượng đối tượng không thể thay đổi trong quá trình thực hiện các lệnh gọi cấp độ ngăn xếp sâu hơn đối với nơi chúng được tạo. Tất cả các kỹ thuật này "thất bại" sức mạnh của chúng trong mọi tình huống trong đó việc tạo và xóa đối tượng là hậu quả của các hoạt động của người dùng, do đó kiểu thời gian chạy của đối tượng không được biên dịch theo thời gian và có thể có các cấu trúc quá mức đề cập đến các đối tượng người dùng đang yêu cầu xóa khỏi lệnh gọi hàm stack stack sâu hơn. Trong trường hợp này, bạn phải:

  • giới thiệu một số kỷ luật về quản lý đối tượng và các cấu trúc giới thiệu liên quan hoặc ...
  • đi bằng cách nào đó đến mặt tối của "thoát khỏi vòng đời dựa trên ngăn xếp thuần túy": đối tượng phải rời khỏi độc lập với các chức năng đã tạo ra chúng. Và phải rời đi ... cho đến khi họ cần .

C ++ isteslf không có bất kỳ cơ chế riêng nào để theo dõi sự kiện đó ( while(are_they_needed)), do đó bạn phải ước chừng:

  1. sử dụng quyền sở hữu chung : cuộc sống của các đối tượng bị ràng buộc với "bộ đếm tham chiếu": hoạt động nếu "quyền sở hữu" có thể được sắp xếp theo thứ bậc, không thành công khi các vòng lặp sở hữu có thể tồn tại. Đây là những gì std :: shared_ptr làm. Và yếu_ptr có thể được sử dụng để phá vỡ vòng lặp. Điều này hoạt động hầu hết thời gian nhưng thất bại trong thiết kế lớn, trong đó nhiều nhà thiết kế làm việc trong các nhóm khác nhau và không có lý do rõ ràng (điều gì đó đến từ một yêu cầu nào đó) về việc ai phải sở hữu những gì (ví dụ điển hình là chuỗi thích kép: là trước khi giới thiệu tiếp theo giới thiệu trước hoặc tiếp theo sở hữu trước đó giới thiệu tiếp theo? Trong một yêu cầu, các giải pháp tho là tương đương, và trong dự án lớn, bạn có nguy cơ trộn lẫn chúng)
  2. Sử dụng một đống rác thu gom : Đơn giản là bạn không quan tâm đến cuộc sống của mình. Bạn chạy bộ sưu tập theo thời gian và những gì unreachabe được coi là "không cần thiết nữa" và ... à ... ôi ... bị phá hủy? Hoàn thiện? Đông cứng?. Có một số trình thu thập GC, nhưng tôi không bao giờ tìm thấy một trình thu thập C ++ thực sự. Hầu hết trong số họ có bộ nhớ miễn phí, không quan tâm đến việc phá hủy đối tượng.
  3. Sử dụng trình thu gom rác nhận biết C ++ , với giao diện phương thức chuẩn phù hợp. Chúc may mắn tìm thấy nó.

Đi đến giải pháp đầu tiên cho giải pháp cuối cùng, lượng cấu trúc dữ liệu phụ trợ cần thiết để quản lý tuổi thọ đối tượng tăng lên, vì thời gian dành cho việc tổ chức và duy trì nó.

Trình thu gom rác có chi phí, shared_ptr có ít hơn, unique_ptr thậm chí còn ít hơn và các đối tượng được quản lý ngăn xếp có rất ít.

shared_ptr"phương sách cuối cùng"?. Không, không phải: phương án cuối cùng là người thu gom rác. shared_ptrthực sự là std::đề xuất cuối cùng Nhưng có thể là giải pháp tốt, nếu bạn đang ở trong tình huống tôi giải thích.


9

Một điều được Herb Sutter đề cập trong một phiên sau đó là mỗi lần bạn sao chép shared_ptr<>sẽ có một sự tăng / giảm liên kết phải xảy ra. Trên mã đa luồng trên hệ thống đa lõi, đồng bộ hóa bộ nhớ không đáng kể. Đưa ra lựa chọn, tốt hơn là sử dụng giá trị ngăn xếp hoặc a unique_ptr<>và chuyển xung quanh các tham chiếu hoặc con trỏ thô.


1
Hoặc vượt qua shared_ptrtham chiếu lvalue hoặc rvalue ...
ronag

8
Vấn đề là, đừng chỉ sử dụng shared_ptrnhư viên đạn bạc sẽ giải quyết tất cả các vấn đề rò rỉ bộ nhớ của bạn chỉ vì nó nằm trong tiêu chuẩn. Đó là một cái bẫy hấp dẫn, nhưng điều quan trọng là phải nhận thức được quyền sở hữu tài nguyên và trừ khi quyền sở hữu đó được chia sẻ, đó shared_ptr<>không phải là lựa chọn tốt nhất.
Nhật thực

Đối với tôi đây là chi tiết ít quan trọng nhất. Xem tối ưu hóa sớm. Trong hầu hết các trường hợp, điều này không nên đưa ra quyết định.
Guy Sirton

1
@gbjbaanb: vâng, họ ở cấp độ cpu, nhưng trên hệ thống đa lõi, bạn đang vô hiệu hóa bộ nhớ cache và buộc các rào cản bộ nhớ.
Nhật thực

4
Trong một dự án trò chơi tôi đã làm việc, chúng tôi thấy rằng sự khác biệt về hiệu năng là rất đáng kể, đến mức chúng tôi cần 2 loại con trỏ được tính lại khác nhau, một loại là chủ đề an toàn, một loại không.
Kylotan

7

Tôi không nhớ nếu "khu nghỉ dưỡng" cuối cùng là từ chính xác mà anh ấy đã sử dụng, nhưng tôi tin rằng ý nghĩa thực sự của những gì anh ấy nói là "sự lựa chọn" cuối cùng: đưa ra các điều kiện sở hữu rõ ràng; unique_ptr, yếu_ptr, shared_ptr và thậm chí con trỏ trần trụi có vị trí của chúng.

Một điều tất cả họ đều đồng ý là tất cả chúng ta (nhà phát triển, tác giả sách, v.v.) đều đang trong "giai đoạn học tập" của C ++ 11 và các mẫu và kiểu dáng đang được xác định.

Lấy ví dụ, Herb giải thích chúng ta nên mong đợi các phiên bản mới của một số sách C ++ bán kết, chẳng hạn như C ++ (Meyers) và C ++ Tiêu chuẩn mã hóa (Sutter & Alexandrescu), một vài năm trong khi kinh nghiệm của ngành và thực hành tốt nhất với C ++ 11 chảo ra.


5

Tôi nghĩ những gì anh ấy nhận được là việc mọi người viết shared_ptr trở nên phổ biến bất cứ khi nào họ có thể viết một con trỏ chuẩn (như một loại thay thế toàn cầu) và nó được sử dụng như một bản sao thay vì thực sự thiết kế hoặc ít nhất là lập kế hoạch cho việc tạo và xóa đối tượng.

Một điều khác mà mọi người quên (bên cạnh nút thắt / cập nhật / mở khóa được đề cập trong tài liệu ở trên), là shared_ptr không giải quyết được các vấn đề về chu kỳ. Bạn vẫn có thể rò rỉ tài nguyên với shared_ptr:

Đối tượng A, chứa một con trỏ chia sẻ với một đối tượng khác Đối tượng A tạo A a1 và A a2 và gán a1.otherA = a2; và a2.otherA = a1; Bây giờ, các con trỏ chia sẻ của đối tượng B được sử dụng để tạo a1, a2 đi ra khỏi phạm vi (giả sử ở cuối hàm). Bây giờ bạn có một rò rỉ - không ai khác đề cập đến a1 và a2, nhưng họ đề cập đến nhau để số lượng ref của họ luôn là 1 và bạn đã bị rò rỉ.

Đó là ví dụ đơn giản, khi điều này xảy ra trong mã thực, nó thường xảy ra theo những cách phức tạp. Có một giải pháp với yếu tố_ptr, nhưng hiện tại có rất nhiều người chỉ chia sẻ ở mọi nơi và thậm chí không biết về vấn đề rò rỉ hoặc thậm chí là yếu_ptr.

Để kết thúc nó: Tôi nghĩ rằng các ý kiến ​​được tham chiếu bởi OP sẽ hiểu rõ điều này:

Bất kể ngôn ngữ nào bạn đang làm việc (được quản lý, không được quản lý hoặc có thứ gì đó ở giữa với số tham chiếu như shared_ptr), bạn cần hiểu và cố ý quyết định về việc tạo đối tượng, thời gian sống và phá hủy.

chỉnh sửa: ngay cả khi điều đó có nghĩa là "không xác định, tôi cần sử dụng shared_ptr", bạn vẫn nghĩ về nó và đang cố tình làm như vậy.


3

Tôi sẽ trả lời từ kinh nghiệm của tôi với Objective-C, một ngôn ngữ mà tất cả các đối tượng được tham chiếu được tính và phân bổ trên heap. Bởi vì có một cách để đối xử với các đối tượng, mọi thứ dễ dàng hơn cho lập trình viên. Điều đó đã cho phép các quy tắc chuẩn được xác định rằng, khi được tuân thủ, đảm bảo độ mạnh của mã và không bị rò rỉ bộ nhớ. Nó cũng có thể giúp tối ưu hóa trình biên dịch thông minh xuất hiện như ARC gần đây (đếm tham chiếu tự động).

Quan điểm của tôi là shared_ptr nên là lựa chọn đầu tiên của bạn chứ không phải là phương sách cuối cùng. Sử dụng tính năng tham chiếu theo mặc định và các tùy chọn khác chỉ khi bạn chắc chắn về những gì bạn đang làm. Bạn sẽ làm việc hiệu quả hơn và mã của bạn sẽ mạnh mẽ hơn.


1

Tôi sẽ cố gắng trả lời câu hỏi:

Làm thế nào chúng ta có thể lập trình mà không có std :: shared_ptr và vẫn quản lý vòng đời đối tượng theo cách an toàn?

C ++ có số lượng lớn các cách khác nhau để làm bộ nhớ, ví dụ:

  1. Sử dụng struct A { MyStruct s1,s2; };thay vì shared_ptr trong phạm vi lớp. Điều này chỉ dành cho các lập trình viên tiên tiến vì nó yêu cầu bạn hiểu cách phụ thuộc hoạt động và yêu cầu khả năng kiểm soát các phụ thuộc đủ để hạn chế chúng vào một cây. Thứ tự các lớp trong tệp tiêu đề là khía cạnh quan trọng của điều này. Có vẻ như việc sử dụng này đã phổ biến với các kiểu c ++ dựng sẵn, nhưng nó được sử dụng với các lớp do lập trình viên định nghĩa dường như ít được sử dụng hơn do các vấn đề phụ thuộc và thứ tự các lớp này. Giải pháp này cũng có vấn đề với sizeof. Các lập trình viên xem các vấn đề trong điều này là một yêu cầu để sử dụng các khai báo chuyển tiếp hoặc #incins không cần thiết và do đó, nhiều lập trình viên sẽ quay trở lại giải pháp kém hơn về con trỏ và sau đó là shared_ptr.
  2. Sử dụng MyClass &find_obj(int i);+ clone () thay vì shared_ptr<MyClass> create_obj(int i);. Nhiều lập trình viên muốn tạo ra các nhà máy để tạo ra các đối tượng mới. shared_ptr là lý tưởng phù hợp cho loại sử dụng này. Vấn đề là nó đã giả định giải pháp quản lý bộ nhớ phức tạp bằng cách sử dụng phân bổ heap / free store, thay vì giải pháp dựa trên stack hoặc object đơn giản hơn. Hệ thống phân cấp lớp C ++ tốt hỗ trợ tất cả các lược đồ quản lý bộ nhớ, không chỉ một trong số chúng. Giải pháp dựa trên tham chiếu có thể hoạt động nếu đối tượng trả về được lưu trữ bên trong đối tượng chứa, thay vì sử dụng biến phạm vi hàm cục bộ. Nên tránh chuyển quyền sở hữu từ nhà máy sang mã người dùng. Sao chép đối tượng sau khi sử dụng find_obj () là cách tốt để xử lý nó - các hàm tạo sao chép bình thường và hàm tạo bình thường (thuộc lớp khác) với tham số điều chỉnh hoặc clone () cho các đối tượng đa hình có thể xử lý nó.
  3. Sử dụng tài liệu tham khảo thay vì con trỏ hoặc shared_ptrs. Mỗi lớp c ++ có các hàm tạo và mỗi thành viên dữ liệu tham chiếu cần được khởi tạo. Việc sử dụng này có thể tránh được việc sử dụng nhiều con trỏ và shared_ptrs. Bạn chỉ cần chọn nếu bộ nhớ của bạn nằm trong đối tượng, hoặc bên ngoài nó, và chọn giải pháp cấu trúc hoặc giải pháp tham chiếu dựa trên quyết định. Các vấn đề với giải pháp này thường liên quan đến việc tránh các tham số của hàm tạo, điều phổ biến nhưng thực tế có vấn đề và hiểu sai về cách các giao diện cho các lớp nên được thiết kế.

"Chuyển quyền sở hữu từ nhà máy sang mã người dùng nên tránh." Và điều gì xảy ra khi điều đó là không thể? "Sử dụng tài liệu tham khảo thay vì con trỏ hoặc shared_ptrs." À, không. Con trỏ có thể được nối lại. Tài liệu tham khảo không thể. Điều này buộc các hạn chế về thời gian xây dựng đối với những gì được lưu trữ trong một lớp. Điều đó không thực tế cho nhiều thứ. Giải pháp của bạn dường như rất cứng nhắc và không phù hợp với nhu cầu của một giao diện và mô hình sử dụng trôi chảy hơn.
Nicol Bolas

@Nicol Bolas: Một khi bạn tuân theo các quy tắc ở trên, các ref sẽ được sử dụng cho các phụ thuộc giữa các đối tượng và không lưu trữ dữ liệu như bạn đề xuất. Các phụ thuộc ổn định hơn dữ liệu, vì vậy chúng tôi không bao giờ gặp phải vấn đề bạn đang xem xét.
tp1

Đây là một ví dụ rất đơn giản. Bạn có một thực thể trò chơi, đó là một đối tượng. Nó cần phải tham chiếu đến một đối tượng khác, đó là một thực thể mục tiêu mà nó cần nói chuyện. Tuy nhiên, mục tiêu có thể thay đổi. Mục tiêu có thể chết ở nhiều điểm khác nhau. Và các thực thể cần phải có khả năng xử lý các trường hợp này. Cách tiếp cận không có con trỏ cứng nhắc của bạn không thể xử lý ngay cả những thứ đơn giản như thay đổi mục tiêu, chứ đừng nói đến mục tiêu đang chết.
Nicol Bolas

@nicol bolas: oh, đó là xử lý khác nhau; giao diện của lớp hỗ trợ nhiều hơn một "thực thể". Thay vì ánh xạ 1: 1 giữa các đối tượng và thực thể, bạn sẽ sử dụng thực thể. Sau đó, các thực thể chết rất dễ dàng bằng cách loại bỏ nó khỏi mảng. Chỉ có một số lượng nhỏ các thực thể trong toàn bộ trò chơi và sự phụ thuộc giữa các mảng không thay đổi thường xuyên :)
tp1

2
Không, unique_ptrlà phù hợp nhất cho các nhà máy. Bạn có thể biến a unique_ptrthành một shared_ptr, nhưng về mặt logic thì không thể đi theo hướng khác.
Ben Voigt
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.