Tôi sử dụng loại con trỏ nào khi nào?


228

Ok, vì vậy lần cuối cùng tôi viết C ++ để kiếm sống, std::auto_ptrtất cả các lib std đều có sẵn, và boost::shared_ptrlà tất cả cơn thịnh nộ. Tôi không bao giờ thực sự nhìn vào các loại con trỏ thông minh khác được cung cấp. Tôi hiểu rằng C ++ 11 hiện cung cấp một số loại boost được đưa ra, nhưng không phải tất cả chúng.

Vậy ai đó có một thuật toán đơn giản để xác định khi nào nên sử dụng con trỏ thông minh nào? Tốt nhất là bao gồm lời khuyên liên quan đến con trỏ câm (con trỏ thô như T*) và phần còn lại của con trỏ thông minh boost. (Một cái gì đó như thế này sẽ là tuyệt vời).



1
Tôi thực sự hy vọng ai đó sẽ đưa ra một sơ đồ tiện dụng đẹp như sơ đồ lựa chọn STL này .
Alok Lưu

1
@Als: Ồ, đó thực sự là một trong những tốt đẹp! Tôi đã hỏi nó.
sbi

6
@Ded repeatator Điều đó thậm chí không gần như là một bản sao. Câu hỏi được liên kết cho biết "Khi nào tôi nên sử dụng một con trỏ thông minh" và câu hỏi này là "Khi nào tôi nên sử dụng các con trỏ thông minh này ?" tức là cái này đang phân loại các cách sử dụng khác nhau của con trỏ thông minh tiêu chuẩn. Các câu hỏi liên kết không làm điều này. Sự khác biệt có vẻ nhỏ nhưng nó là lớn.
Rapptz

Câu trả lời:


183

Chia sẻ quyền sở hữu:
Các shared_ptrweak_ptrtiêu chuẩn áp dụng là khá nhiều giống như họ đối Boost . Sử dụng chúng khi bạn cần chia sẻ tài nguyên và không biết tài nguyên nào sẽ là người cuối cùng còn sống. Sử dụng weak_ptrđể quan sát tài nguyên được chia sẻ mà không ảnh hưởng đến tuổi thọ của nó, không phá vỡ chu kỳ. Chu kỳ shared_ptrkhông thường xảy ra - hai tài nguyên không thể sở hữu lẫn nhau.

Lưu ý rằng Boost cung cấp thêm shared_array, có thể là một sự thay thế phù hợp shared_ptr<std::vector<T> const>.

Tiếp theo, Boost cung cấp intrusive_ptr, đây là một giải pháp nhẹ nếu tài nguyên của bạn cung cấp quản lý được tính tham chiếu và bạn muốn áp dụng nó theo nguyên tắc RAII. Điều này đã không được thông qua bởi các tiêu chuẩn.

Quyền sở hữu duy nhất:
Boost cũng có một scoped_ptr, không thể sao chép và bạn không thể chỉ định một deleter. std::unique_ptrboost::scoped_ptrtrên steroid và nên là lựa chọn mặc định của bạn khi bạn cần một con trỏ thông minh . Nó cho phép bạn chỉ định một deleter trong các đối số mẫu của nó và có thể di chuyển , không giống như boost::scoped_ptr. Nó cũng hoàn toàn có thể sử dụng được trong các thùng chứa STL miễn là bạn không sử dụng các thao tác cần các loại có thể sao chép (rõ ràng).

Lưu ý một lần nữa, Boost đó có một phiên bản mảng: scoped_arraytiêu chuẩn thống nhất bằng cách yêu cầu std::unique_ptr<T[]>chuyên môn hóa một phần sẽ delete[]thay cho con trỏ delete(với default_deleter). std::unique_ptr<T[]>cũng cung cấp operator[]thay vì operator*operator->.

Lưu ý rằng std::auto_ptrvẫn còn trong tiêu chuẩn, nhưng nó không được chấp nhận . §D.10 [depr.auto.ptr]

Mẫu lớp auto_ptrkhông được dùng nữa. [ Lưu ý: Mẫu lớp unique_ptr(20.7.1) cung cấp giải pháp tốt hơn. -End note ]

Không có quyền sở hữu:
Sử dụng con trỏ câm (con trỏ thô) hoặc tham chiếu cho các tham chiếu không sở hữu tài nguyên và khi bạn biết rằng tài nguyên sẽ tồn tại lâu hơn đối tượng / phạm vi tham chiếu. Thích tham chiếu và sử dụng con trỏ thô khi bạn cần nullable hoặc resettable.

Nếu bạn muốn tham chiếu không sở hữu tài nguyên, nhưng bạn không biết liệu tài nguyên đó có tồn tại lâu hơn đối tượng tham chiếu tài nguyên đó hay không, hãy đóng gói tài nguyên trong một shared_ptrvà sử dụng weak_ptr- bạn có thể kiểm tra xem cha mẹ shared_ptrcó còn sống lockkhông trả về một shared_ptrgiá trị khác không nếu tài nguyên vẫn tồn tại. Nếu muốn kiểm tra xem tài nguyên đã chết, hãy sử dụng expired. Cả hai có thể giống nhau, nhưng rất khác nhau khi thực hiện đồng thời, vì expiredchỉ đảm bảo giá trị trả về của nó cho câu lệnh đơn đó. Một thử nghiệm dường như vô hại như

if(!wptr.expired())
  something_assuming_the_resource_is_still_alive();

là một điều kiện chủng tộc tiềm năng.


1
Trong trường hợp không có quyền sở hữu, có lẽ bạn nên thích tham chiếu đến các con trỏ trừ khi bạn không cần quyền sở hữu và khả năng phục hồi khi các tham chiếu sẽ không cắt nó, thậm chí sau đó bạn có thể muốn xem xét viết lại đối tượng ban đầu thành một shared_ptrvà con trỏ không sở hữu một weak_ptr...
David Rodríguez - dribeas

2
Tôi không có nghĩa là tham chiếu đến con trỏ , mà là tham chiếu thay vì con trỏ. Nếu không có quyền sở hữu, trừ khi bạn cần khả năng phục hồi (hoặc không có giá trị, nhưng không có khả năng thiết lập lại sẽ khá hạn chế), bạn có thể sử dụng một tham chiếu đơn giản thay vì con trỏ ở vị trí đầu tiên.
David Rodríguez - dribeas

1
@David: À, tôi hiểu rồi. :) Vâng, tài liệu tham khảo không tệ cho điều đó, cá nhân tôi cũng thích chúng trong những trường hợp như vậy. Tôi sẽ thêm chúng.
Xèo

1
@Xeo: shared_array<T>là một thay thế để shared_ptr<T[]>không shared_ptr<vector<T>>: nó không thể phát triển.
R. Martinho Fernandes

1
@GregroyCurrie: Đó là ... chính xác những gì tôi đã viết? Tôi đã nói đó là một ví dụ về tình trạng chủng tộc tiềm năng.
Xèo

127

Quyết định sử dụng con trỏ thông minh nào là câu hỏi về quyền sở hữu . Khi nói đến quản lý tài nguyên, đối tượng A sở hữu đối tượng B nếu nó kiểm soát vòng đời của đối tượng B. Ví dụ, các biến thành viên được sở hữu bởi các đối tượng tương ứng của chúng vì thời gian tồn tại của các biến thành viên được gắn với vòng đời của đối tượng. Bạn chọn con trỏ thông minh dựa trên cách đối tượng được sở hữu.

Lưu ý rằng quyền sở hữu trong một hệ thống phần mềm tách biệt với quyền sở hữu như chúng ta sẽ nghĩ về nó bên ngoài phần mềm. Ví dụ, một người có thể "sở hữu" nhà của họ, nhưng điều đó không nhất thiết có nghĩa là một Personđối tượng có quyền kiểm soát vòng đời của một Houseđối tượng. Kết hợp các khái niệm trong thế giới thực này với các khái niệm phần mềm là một cách chắc chắn để tự lập trình vào một lỗ hổng.


Nếu bạn có quyền sở hữu duy nhất của đối tượng, sử dụng std::unique_ptr<T>.

Nếu bạn đã chia sẻ quyền sở hữu đối tượng ...
- Nếu không có chu kỳ sở hữu, hãy sử dụng std::shared_ptr<T>.
- Nếu có chu kỳ, xác định "hướng" và sử dụng std::shared_ptr<T>theo hướng này và hướng std::weak_ptr<T>khác.

Nếu đối tượng sở hữu bạn, nhưng có khả năng không có chủ sở hữu, hãy sử dụng các con trỏ bình thường T*(ví dụ: con trỏ cha mẹ).

Nếu đối tượng sở hữu bạn (hoặc nếu không có sự tồn tại được đảm bảo), hãy sử dụng tài liệu tham khảo T&.


Hãy cẩn thận: Hãy nhận biết các chi phí của con trỏ thông minh. Trong môi trường hạn chế về bộ nhớ hoặc hiệu năng, có thể có ích khi chỉ sử dụng các con trỏ bình thường với sơ đồ thủ công hơn để quản lý bộ nhớ.

Các chi phí:

  • Nếu bạn có một deleter tùy chỉnh (ví dụ: bạn sử dụng nhóm phân bổ) thì điều này sẽ phát sinh chi phí trên mỗi con trỏ có thể dễ dàng tránh được bằng cách xóa thủ công.
  • std::shared_ptrcó chi phí gia tăng số tham chiếu trên bản sao, cộng với sự giảm dần về sự phá hủy, sau đó là kiểm tra số 0 với việc xóa đối tượng bị giữ. Tùy thuộc vào việc triển khai, điều này có thể làm sai mã của bạn và gây ra các vấn đề về hiệu suất.
  • Thời gian biên dịch. Như với tất cả các mẫu, con trỏ thông minh đóng góp tiêu cực vào thời gian biên dịch.

Ví dụ:

struct BinaryTree
{
    Tree* m_parent;
    std::unique_ptr<BinaryTree> m_children[2]; // or use std::array...
};

Cây nhị phân không sở hữu cha mẹ của nó, nhưng sự tồn tại của cây ngụ ý sự tồn tại của cha mẹ của nó (hoặc nullptrcho gốc), do đó sử dụng một con trỏ bình thường. Cây nhị phân (có giá trị ngữ nghĩa) có quyền sở hữu duy nhất đối với con của nó, vì vậy chúng là std::unique_ptr.

struct ListNode
{
    std::shared_ptr<ListNode> m_next;
    std::weak_ptr<ListNode> m_prev;
};

Ở đây, nút danh sách sở hữu các danh sách tiếp theo và trước đó, vì vậy chúng tôi xác định hướng và sử dụng shared_ptrcho tiếp theo và weak_ptrtrước để phá vỡ chu kỳ.


3
Ví dụ về cây nhị phân, một số người sẽ đề nghị sử dụng shared_ptr<BinaryTree>cho trẻ em và weak_ptr<BinaryTree>cho mối quan hệ cha mẹ.
David Rodríguez - dribeas

@ DavidRodríguez-dribeas: Nó phụ thuộc vào việc Cây có giá trị ngữ nghĩa hay không. Nếu mọi người sẽ tham chiếu cây của bạn ra bên ngoài ngay cả khi cây nguồn bị phá hủy thì có, kết hợp con trỏ chia sẻ / yếu sẽ là tốt nhất.
Peter Alexander

Nếu một đối tượng sở hữu bạn và được đảm bảo tồn tại thì tại sao không tham khảo.
Martin York

1
Nếu bạn sử dụng tài liệu tham khảo, bạn không bao giờ có thể thay đổi cha mẹ, điều này có thể hoặc không thể cản trở thiết kế. Đối với cây cân bằng, điều đó sẽ cản trở.
Vịt Mooing

3
+1 nhưng bạn nên thêm định nghĩa về "quyền sở hữu" trên dòng đầu tiên. Tôi thường thấy mình phải nói rõ rằng đó là về sự sống và cái chết của đối tượng, chứ không phải quyền sở hữu theo nghĩa cụ thể hơn về miền.
Klaim

19

Sử dụng unique_ptr<T>tất cả thời gian trừ khi bạn cần đếm tham chiếu, trong trường hợp đó sử dụng shared_ptr<T>(và trong trường hợp rất hiếm, weak_ptr<T>để ngăn chặn chu kỳ tham chiếu). Trong hầu hết mọi trường hợp, quyền sở hữu duy nhất có thể chuyển nhượng là tốt.

Con trỏ thô: Chỉ tốt nếu bạn cần trả về covariant, trỏ không sở hữu có thể xảy ra. Chúng không thực sự hữu ích nếu không.

Con trỏ mảng: unique_ptrcó một chuyên môn T[]tự động gọi delete[]kết quả, vì vậy bạn có thể làm một cách an toàn unique_ptr<int[]> p(new int[42]);chẳng hạn. shared_ptrbạn vẫn cần một trình duyệt tùy chỉnh, nhưng bạn sẽ không cần một con trỏ mảng duy nhất được chia sẻ hoặc duy nhất. Tất nhiên, những thứ như vậy thường được thay thế tốt nhất bằng cách std::vectornào. Thật không may shared_ptr, không cung cấp chức năng truy cập mảng, vì vậy bạn vẫn phải gọi thủ công get(), nhưng unique_ptr<T[]>cung cấp operator[]thay vì operator*operator->. Trong mọi trường hợp, bạn phải tự kiểm tra giới hạn. Điều này làm cho shared_ptrít thân thiện hơn với người dùng, mặc dù có thể nói là lợi thế chung và không phụ thuộc vào Boost unique_ptrshared_ptrngười chiến thắng một lần nữa.

Con trỏ phạm vi: Được thực hiện không liên quan bởi unique_ptr, giống như auto_ptr.

Thật sự không còn gì nữa. Trong C ++ 03 không có ngữ nghĩa di chuyển thì tình huống này rất phức tạp, nhưng trong C ++ 11, lời khuyên rất đơn giản.

Vẫn còn sử dụng cho các con trỏ thông minh khác, như intrusive_ptrhoặc interprocess_ptr. Tuy nhiên, chúng rất thích hợp và hoàn toàn không cần thiết trong trường hợp chung.


Ngoài ra, con trỏ thô cho lặp đi lặp lại. Và đối với bộ đệm tham số đầu ra, trong đó bộ đệm được sở hữu bởi người gọi.
Ben Voigt

Hmm, theo cách tôi đọc, đó là những tình huống vừa trở lại vừa không sở hữu. Viết lại có thể là tốt nếu bạn có nghĩa là liên minh chứ không phải là giao lộ. Tôi cũng muốn nói rằng lặp đi lặp lại cũng đáng được đề cập đặc biệt.
Ben Voigt

2
std::unique_ptr<T[]>cung cấp operator[]thay vì operator*operator->. Đúng là bạn vẫn cần phải tự kiểm tra ràng buộc.
Xèo

8

Các trường hợp khi sử dụng unique_ptr:

  • Phương pháp nhà máy
  • Các thành viên là con trỏ (bao gồm pimpl)
  • Lưu trữ con trỏ trong stlers conters (để tránh di chuyển)
  • Sử dụng các đối tượng động lớn cục bộ

Các trường hợp khi sử dụng shared_ptr:

  • Chia sẻ các đối tượng qua các chủ đề
  • Chia sẻ đối tượng nói chung

Các trường hợp khi sử dụng weak_ptr:

  • Bản đồ lớn hoạt động như một tài liệu tham khảo chung (ví dụ: bản đồ của tất cả các ổ cắm mở)

Hãy chỉnh sửa và thêm nhiều hơn nữa


Tôi thực sự thích câu trả lời của bạn hơn khi bạn đưa ra kịch bản.
Nicholas Humphrey
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.