Có những triển khai con trỏ thông minh nào trong C ++?


121

So sánh, Ưu điểm, Nhược điểm và Khi nào Sử dụng?

Đây là một bản spin-off từ một chuỗi thu thập rác nơi tôi nghĩ là một câu trả lời đơn giản đã tạo ra rất nhiều nhận xét về một số triển khai con trỏ thông minh cụ thể nên có vẻ đáng để bắt đầu một bài đăng mới.

Cuối cùng, câu hỏi đặt ra là các triển khai khác nhau của con trỏ thông minh trong C ++ ngoài kia là gì và chúng so sánh như thế nào? Chỉ là những ưu và khuyết điểm đơn giản hoặc những ngoại lệ và tìm kiếm một thứ gì đó mà bạn có thể nghĩ là nên làm việc.

Tôi đã đăng một số triển khai mà tôi đã sử dụng hoặc ít nhất là lược bỏ và coi việc sử dụng như một câu trả lời bên dưới và sự hiểu biết của tôi về sự khác biệt và tương đồng của chúng có thể không chính xác 100%, vì vậy hãy kiểm tra thực tế hoặc sửa cho tôi nếu cần.

Mục đích là để tìm hiểu về một số đối tượng và thư viện mới hoặc sửa cách sử dụng và hiểu biết của tôi về các triển khai hiện có đã được sử dụng rộng rãi và kết thúc bằng một tài liệu tham khảo phù hợp cho những người khác.


5
Tôi nghĩ rằng điều này nên được đăng lại như một câu trả lời cho câu hỏi này và câu hỏi được thực hiện thành một câu hỏi thực tế. Nếu không, tôi cảm thấy mọi người sẽ kết luận điều này là "không phải là một câu hỏi thực sự".
strager

3
Có tất cả các loại con trỏ thông minh khác, ví dụ như con trỏ thông minh ATL hoặc OpenSceneGraphosg::ref_ptr .
James McNellis

11
Có một câu hỏi ở đây?
Cody Grey

6
Tôi nghĩ rằng bạn đã hiểu sai std::auto_ptr. std::auto_ptr_reflà một chi tiết thiết kế của std::auto_ptr. std::auto_ptrkhông liên quan gì đến việc thu gom rác, mục đích chính là đặc biệt cho phép chuyển quyền sở hữu ngoại lệ một cách an toàn, đặc biệt là trong các tình huống gọi hàm và trả về hàm. std::unique_ptrchỉ có thể giải quyết các "vấn đề" mà bạn trích dẫn với các vùng chứa tiêu chuẩn vì C ++ đã thay đổi để cho phép phân biệt giữa di chuyển và sao chép và các vùng chứa tiêu chuẩn đã thay đổi để tận dụng lợi thế này.
CB Bailey

3
Bạn nói rằng bạn không phải là một chuyên gia về các con trỏ thông minh nhưng bản tóm tắt của bạn khá đầy đủ và chính xác (ngoại trừ sự phân minh nhỏ về việc auto_ptr_reftrở thành chi tiết triển khai). Tuy nhiên, tôi đồng ý rằng bạn nên đăng bài này như một câu trả lời và định dạng lại câu hỏi để trở thành một câu hỏi thực tế. Điều này sau đó có thể phục vụ như một tài liệu tham khảo trong tương lai.
Konrad Rudolph

Câu trả lời:


231

C ++ 03

std::auto_ptr- Có lẽ một trong những bản gốc mà nó mắc phải hội chứng bản thảo đầu tiên chỉ cung cấp các phương tiện thu gom rác hạn chế. Nhược điểm đầu tiên là nó kêu gọi deletesự phá hủy khiến chúng không được chấp nhận để giữ các đối tượng được cấp phát mảng ( new[]). Nó có quyền sở hữu con trỏ nên hai con trỏ tự động không được chứa cùng một đối tượng. Việc gán sẽ chuyển quyền sở hữu và đặt lại con trỏ tự động rvalue thành con trỏ null. Điều này có lẽ dẫn đến nhược điểm tồi tệ nhất; chúng không thể được sử dụng trong các vùng chứa STL do không thể sao chép đã nói ở trên. Cú đánh cuối cùng đối với bất kỳ trường hợp sử dụng nào là chúng dự kiến ​​sẽ không được chấp nhận trong tiêu chuẩn tiếp theo của C ++.

std::auto_ptr_ref- Đây không phải là một con trỏ thông minh mà nó thực sự là một chi tiết thiết kế được sử dụng cùng với std::auto_ptrđể cho phép sao chép và gán trong một số tình huống nhất định. Cụ thể, nó có thể được sử dụng để chuyển đổi một giá trị không phải const std::auto_ptrthành một giá trị bằng cách sử dụng thủ thuật Colvin-Gibbons còn được gọi là một hàm tạo di chuyển để chuyển quyền sở hữu.

Ngược lại, có lẽ std::auto_ptrnó không thực sự được sử dụng như một con trỏ thông minh cho mục đích chung để thu gom rác tự động. Hầu hết những hiểu biết hạn chế và các giả định của tôi đều dựa trên Cách sử dụng auto_ptr hiệu quả của Herb Sutter và tôi sử dụng nó thường xuyên mặc dù không phải lúc nào cũng theo cách tối ưu nhất.


C ++ 11

std::unique_ptr- Đây là người bạn của chúng tôi, người sẽ thay thế std::auto_ptrnó sẽ khá giống ngoại trừ những cải tiến quan trọng để sửa các điểm yếu std::auto_ptrnhư làm việc với mảng, bảo vệ giá trị thông qua phương thức khởi tạo bản sao riêng, có thể sử dụng được với vùng chứa STL và thuật toán, v.v. Vì đó là chi phí hiệu suất và dấu chân bộ nhớ bị hạn chế, đây là một ứng cử viên lý tưởng để thay thế, hoặc có lẽ được mô tả một cách khéo léo hơn là sở hữu con trỏ thô. Vì "duy nhất" ngụ ý chỉ có một chủ sở hữu của con trỏ giống như trước đó std::auto_ptr.

std::shared_ptr- Tôi tin rằng điều này dựa trên TR1 và boost::shared_ptrnhưng được cải thiện để bao gồm răng cưa và số học con trỏ. Tóm lại, nó bao bọc một con trỏ thông minh được tính tham chiếu xung quanh một đối tượng được phân bổ động. Vì "shared" ngụ ý rằng con trỏ có thể được sở hữu bởi nhiều hơn một con trỏ được chia sẻ khi tham chiếu cuối cùng của con trỏ được chia sẻ cuối cùng vượt ra ngoài phạm vi thì đối tượng sẽ bị xóa một cách thích hợp. Đây cũng là các luồng an toàn và có thể xử lý các loại không hoàn chỉnh trong hầu hết các trường hợp. std::make_sharedcó thể được sử dụng để xây dựng std::shared_ptrphân bổ một đống một cách hiệu quả bằng trình cấp phát mặc định.

std::weak_ptr- Tương tự như vậy dựa trên TR1 và boost::weak_ptr. Đây là một tham chiếu đến một đối tượng thuộc sở hữu của a std::shared_ptrvà do đó sẽ không ngăn cản việc xóa đối tượng nếu std::shared_ptrsố lượng tham chiếu giảm xuống không. Để có quyền truy cập vào con trỏ thô, trước tiên bạn sẽ cần truy cập std::shared_ptrbằng cách gọi locknó sẽ trả về giá trị trống std::shared_ptrnếu con trỏ sở hữu đã hết hạn và đã bị phá hủy. Điều này chủ yếu hữu ích để tránh số lượng tham chiếu bị treo vô thời hạn khi sử dụng nhiều con trỏ thông minh.


Tăng cường

boost::shared_ptr- Có thể là dễ sử dụng nhất trong các tình huống khác nhau (STL, PIMPL, RAII, v.v.), đây là con trỏ thông minh được tính tham chiếu được chia sẻ. Tôi đã nghe một vài lời phàn nàn về hiệu suất và chi phí trong một số tình huống nhưng tôi phải bỏ qua chúng vì tôi không thể nhớ lập luận là gì. Rõ ràng nó đã đủ phổ biến để trở thành một đối tượng C ++ tiêu chuẩn đang chờ xử lý và không có nhược điểm nào so với tiêu chuẩn liên quan đến con trỏ thông minh.

boost::weak_ptr- Giống như mô tả trước đây std::weak_ptr, dựa trên việc triển khai này, điều này cho phép một tham chiếu không sở hữu đến a boost::shared_ptr. Bạn không ngạc nhiên khi gọi lock()để truy cập con trỏ chia sẻ "mạnh" và phải kiểm tra để đảm bảo rằng nó hợp lệ vì nó có thể đã bị phá hủy. Chỉ cần đảm bảo không lưu trữ con trỏ chia sẻ được trả về và để nó ra khỏi phạm vi ngay sau khi bạn hoàn thành việc đó, nếu không bạn sẽ quay lại ngay vấn đề tham chiếu tuần hoàn nơi số lượng tham chiếu của bạn sẽ bị treo và các đối tượng sẽ không bị phá hủy.

boost::scoped_ptr- Đây là một lớp con trỏ thông minh đơn giản với ít chi phí có thể được thiết kế để thay thế hoạt động tốt hơn boost::shared_ptrkhi có thể sử dụng. Nó có thể so sánh std::auto_ptrđặc biệt với thực tế là nó không thể được sử dụng một cách an toàn như một phần tử của vùng chứa STL hoặc với nhiều con trỏ đến cùng một đối tượng.

boost::intrusive_ptr- Tôi chưa bao giờ sử dụng cái này nhưng theo hiểu biết của tôi, nó được thiết kế để sử dụng khi tạo các lớp tương thích với con trỏ thông minh của riêng bạn. Bạn cần tự thực hiện việc đếm tham chiếu, bạn cũng sẽ cần triển khai một vài phương thức nếu bạn muốn lớp của mình là chung, hơn nữa bạn phải triển khai an toàn luồng của riêng mình. Về mặt tích cực, điều này có thể cung cấp cho bạn cách tùy chỉnh nhất để chọn và chọn chính xác mức độ "thông minh" mà bạn muốn. intrusive_ptrthường hiệu quả hơn shared_ptrvì nó cho phép bạn có một phân bổ heap cho mỗi đối tượng. (cảm ơn Arvid)

boost::shared_array- Đây là một boost::shared_ptrmảng. Về cơ bản new [], operator[]và tất nhiên delete []là được đưa vào. Điều này có thể được sử dụng trong các thùng chứa STL và theo như tôi biết thì mọi thứ đều boost:shared_ptrlàm được mặc dù bạn không thể sử dụng boost::weak_ptrvới những thứ này. Tuy nhiên, bạn có thể sử dụng một cách khác boost::shared_ptr<std::vector<>>cho chức năng tương tự và để lấy lại khả năng sử dụng boost::weak_ptrcho các tài liệu tham khảo.

boost::scoped_array- Đây là một boost::scoped_ptrmảng. Như với boost::shared_arraytất cả tính tốt của mảng cần thiết được đưa vào. Cái này không thể sao chép và vì vậy không thể sử dụng trong các vùng chứa STL. Tôi đã tìm thấy hầu hết mọi nơi bạn muốn sử dụng cái này mà bạn có thể chỉ cần sử dụng std::vector. Tôi chưa bao giờ xác định cái nào thực sự nhanh hơn hoặc ít chi phí hơn nhưng mảng có phạm vi này dường như ít liên quan hơn nhiều so với một vector STL. boost::arrayThay vào đó, khi bạn muốn tiếp tục phân bổ trên ngăn xếp .


Qt

QPointer- Được giới thiệu trong Qt 4.0, đây là một con trỏ thông minh "yếu" chỉ hoạt động với QObjectvà các lớp dẫn xuất, mà trong Qt framework là hầu hết mọi thứ nên đó không thực sự là một hạn chế. Tuy nhiên, có những hạn chế cụ thể là nó không cung cấp con trỏ "mạnh" và mặc dù bạn có thể kiểm tra xem đối tượng bên dưới có hợp lệ hay isNull()không, bạn có thể thấy đối tượng của mình bị phá hủy ngay sau khi bạn vượt qua kiểm tra đó, đặc biệt là trong môi trường đa luồng. Qt mọi người coi điều này là không được chấp nhận, tôi tin.

QSharedDataPointer- Đây là một con trỏ thông minh "mạnh" có khả năng so sánh với boost::intrusive_ptrmặc dù nó có một số tích hợp an toàn luồng nhưng nó yêu cầu bạn bao gồm các phương pháp đếm tham chiếu ( refderef) mà bạn có thể thực hiện bằng cách phân lớp QSharedData. Như với phần lớn Qt, các đối tượng được sử dụng tốt nhất thông qua tính kế thừa phong phú và phân lớp mọi thứ dường như là thiết kế dự kiến.

QExplicitlySharedDataPointer- Rất giống với QSharedDataPointerngoại trừ nó không ngầm gọi detach(). Tôi gọi đây là phiên bản 2.0 QSharedDataPointervì sự gia tăng nhẹ kiểm soát chính xác thời điểm tách ra sau khi số lượng tham chiếu giảm xuống 0 không đặc biệt có giá trị đối với một đối tượng hoàn toàn mới.

QSharedPointer- Đếm tham chiếu nguyên tử, an toàn chuỗi, con trỏ có thể chia sẻ, xóa tùy chỉnh (hỗ trợ mảng), giống như mọi thứ mà một con trỏ thông minh nên có. Đây là những gì tôi chủ yếu sử dụng như một con trỏ thông minh trong Qt và tôi thấy nó có thể so sánh được với boost:shared_ptrmặc dù có lẽ chi phí cao hơn đáng kể như nhiều đối tượng trong Qt.

QWeakPointer- Bạn có cảm thấy một mô hình đang quay lại không? Cũng giống như std::weak_ptrboost::weak_ptrđiều này được sử dụng cùng với QSharedPointerkhi bạn cần tham chiếu giữa hai con trỏ thông minh, nếu không sẽ khiến các đối tượng của bạn không bao giờ bị xóa.

QScopedPointer- Tên này trông cũng quen thuộc và trên thực tế nó dựa trên thực tế boost::scoped_ptrkhông giống như phiên bản Qt của con trỏ chia sẻ và con trỏ yếu. Nó có chức năng cung cấp con trỏ thông minh cho một chủ sở hữu duy nhất mà không có chi phí QSharedPointerlàm cho nó phù hợp hơn với khả năng tương thích, mã an toàn ngoại lệ và tất cả những thứ bạn có thể sử dụng std::auto_ptrhoặc boost::scoped_ptrcho.


1
hai điều tôi nghĩ là đáng đề cập: intrusive_ptrthường hiệu quả hơn shared_ptr, vì nó cho phép bạn có một phân bổ heap duy nhất cho mỗi đối tượng. shared_ptrtrong trường hợp chung sẽ phân bổ một đối tượng heap nhỏ riêng biệt cho các bộ đếm tham chiếu. std::make_sharedcó thể được sử dụng để đạt được điều tốt nhất của cả hai thế giới. shared_ptrchỉ với một phân bổ heap duy nhất.
Arvid

Tôi có một câu hỏi có lẽ không liên quan: Có thể thực hiện thu gom rác chỉ bằng cách thay thế tất cả các con trỏ bằng shared_ptrs không? (Không kể việc giải quyết các tài liệu tham khảo cyclic)
Seth Carnegie

@Seth Carnegie: Không phải tất cả các con trỏ sẽ trỏ đến thứ gì đó được phân bổ trên cửa hàng miễn phí.
Trong silico

2
@the_mandrill Nhưng nó hoạt động nếu trình hủy của lớp sở hữu được xác định trong một đơn vị dịch riêng biệt (.cpp-file) so với mã máy khách, mà trong thành ngữ Pimpl vẫn được đưa ra. Bởi vì đơn vị dịch này thường biết định nghĩa đầy đủ của Pimpl và do đó trình hủy của nó (khi nó hủy auto_ptr) sẽ hủy chính xác Pimpl. Tôi cũng đã lo sợ về điều này khi tôi nhìn thấy những cảnh báo đó, nhưng tôi đã thử nó và nó hoạt động (bộ hủy của Pimpl được gọi). Tái bút: vui lòng sử dụng @ -syntax để tôi xem bất kỳ câu trả lời nào.
Christian Rau

2
Tính hữu dụng của danh sách đã được tăng lên bằng cách thêm các liên kết thích hợp vào tài liệu.
ulidtko


1

Ngoài những điều được đưa ra, cũng có một số định hướng an toàn:

SaferCPlusPlus

mse::TRefCountingPointerlà một con trỏ thông minh đếm tham chiếu như thế nào std::shared_ptr. Sự khác biệt mse::TRefCountingPointerlà an toàn hơn, nhỏ hơn và nhanh hơn, nhưng không có bất kỳ cơ chế an toàn nào. Và nó có các phiên bản "không null" và "cố định" (không thể nhắm mục tiêu lại) có thể được giả định một cách an toàn là luôn trỏ đến một đối tượng được cấp phát hợp lệ. Vì vậy, về cơ bản, nếu đối tượng mục tiêu của bạn được chia sẻ giữa các luồng không đồng bộ thì hãy sử dụng std::shared_ptr, ngược lại thì mse::TRefCountingPointertối ưu hơn.

mse::TScopeOwnerPointertương tự như boost::scoped_ptr, nhưng hoạt động cùng với mse::TScopeFixedPointerkiểu quan hệ con trỏ "mạnh-yếu" như std::shared_ptrstd::weak_ptr.

mse::TScopeFixedPointertrỏ đến các đối tượng được cấp phát trên ngăn xếp hoặc con trỏ "sở hữu" của chúng được cấp phát trên ngăn xếp. Nó (cố ý) bị hạn chế về chức năng của nó để tăng cường an toàn trong thời gian biên dịch mà không tốn thời gian chạy. Điểm của con trỏ "phạm vi" về cơ bản là để xác định một tập hợp các tình huống đủ đơn giản và xác định để không có cơ chế an toàn (thời gian chạy) nào là cần thiết.

mse::TRegisteredPointerhoạt động giống như một con trỏ thô, ngoại trừ việc giá trị của nó được tự động đặt thành null_ptr khi đối tượng đích bị phá hủy. Nó có thể được sử dụng như một sự thay thế chung cho các con trỏ thô trong hầu hết các tình huống. Giống như một con trỏ thô, nó không có bất kỳ sự an toàn nội tại nào. Nhưng đổi lại, nó không có vấn đề gì với việc nhắm mục tiêu các đối tượng được phân bổ trên ngăn xếp (và thu được lợi ích về hiệu suất tương ứng). Khi kích hoạt kiểm tra thời gian chạy, con trỏ này an toàn khỏi việc truy cập bộ nhớ không hợp lệ. Vì mse::TRegisteredPointercó hành vi tương tự như con trỏ thô khi trỏ đến các đối tượng hợp lệ, nó có thể bị "vô hiệu hóa" (tự động thay thế bằng con trỏ thô tương ứng) bằng chỉ thị thời gian biên dịch, cho phép nó được sử dụng để giúp bắt lỗi trong gỡ lỗi / kiểm tra. / chế độ beta trong khi không phải chịu chi phí chung trong chế độ phát hành.

Đây là một bài viết mô tả lý do tại sao và làm thế nào để sử dụng chúng. (Lưu ý, phích cắm vô liêm sỉ.)

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.