2009-02-02 58 views
12

Tôi đã đánh giá các triển khai con trỏ thông minh khác nhau (wow, có một LOT trên đó) và dường như với tôi rằng hầu hết chúng có thể được phân loại thành hai phân loại rộng:Cách tốt nhất để triển khai con trỏ thông minh trong C++ là gì?

1) Danh mục này sử dụng thừa kế trên các đối tượng được tham chiếu để chúng có số lượng tham chiếu và thường lên() và xuống() (hoặc tương đương của chúng) được triển khai. IE, để sử dụng con trỏ thông minh, các đối tượng mà bạn trỏ vào phải thừa kế từ một số lớp mà triển khai thực hiện cung cấp.

2) Danh mục này sử dụng đối tượng phụ để giữ số lượng tham chiếu. Ví dụ, thay vì trỏ con trỏ thông minh vào đúng đối tượng, nó thực sự trỏ vào đối tượng siêu dữ liệu này ... Ai có một số tham chiếu và lên() và down() triển khai (và thường cung cấp cơ chế cho con trỏ đến nhận được tại các đối tượng thực tế được trỏ đến, để con trỏ thông minh có thể thực hiện đúng toán tử ->()).

Bây giờ, 1 có nhược điểm là nó buộc tất cả các đối tượng bạn muốn tham chiếu đếm để kế thừa từ một tổ tiên chung, và điều này có nghĩa là bạn không thể sử dụng nó để tham chiếu đối tượng đếm mà bạn không có quyền kiểm soát qua mã nguồn.

2 có vấn đề vì số được lưu trữ trong đối tượng khác, nếu bạn có tình huống con trỏ đến đối tượng được tính tham chiếu hiện tại đang được chuyển đổi thành tham chiếu, có thể bạn đã gặp lỗi (IE, vì đếm không nằm trong đối tượng thực sự, không có cách nào để tham chiếu mới nhận số đếm ... ref để sao chép bản dựng hoặc gán là tốt, bởi vì chúng có thể chia sẻ đối tượng đếm, nhưng nếu bạn phải chuyển đổi từ con trỏ, bạn hoàn toàn bị hosed) ...

Bây giờ, khi tôi hiểu nó, hãy tăng :: shared_pointer sử dụng cơ chế 2, hoặc cái gì đó tương tự ... Điều đó nói rằng, tôi hoàn toàn không thể quyết định tệ hơn! Tôi đã từng sử dụng cơ chế 1, trong mã sản xuất ... Có ai có kinh nghiệm với cả hai kiểu dáng? Hoặc có lẽ có một cách khác tốt hơn cả hai thứ này?

Trả lời

27

  1. Đừng "cách tốt nhất để thực hiện con trỏ thông minh trong C++ là gì"! Sử dụng con trỏ thông minh hiện có, được thử nghiệm tốt, chẳng hạn như tăng :: shared_ptr hoặc std :: tr1 :: shared_ptr (std :: unique_ptr và std :: shared_ptr with C++ 11)
  2. Nếu bạn phải, hãy nhớ:
    1. sử dụng an toàn-bool thành ngữ
    2. cung cấp một nhà khai thác>
    3. cung cấp bảo lãnh ngoại lệ mạnh mẽ
    4. tài liệu yêu cầu ngoại lệ lớp học của bạn làm trên deleter
    5. sử dụng cắt-chỉnh sửa-swap nếu có thể để thực hiện gua mạnh mẽ ngoại lệ rantee
    6. tài liệu cho dù bạn xử lý đa luồng đúng
    7. ghi đơn vị mở rộng kiểm tra
    8. thực hiện chuyển đổi sang cơ sở theo một cách như vậy mà nó sẽ xóa vào loại con trỏ có nguồn gốc (policied con trỏ thông minh/deleter động gợi ý thông minh)
    9. hỗ trợ nhận được quyền truy cập vào con trỏ thô
    10. xem xét chi phí/benifit cung cấp gợi ý yếu để phá vỡ chu kỳ
    11. cung cấp khai thác đúc thích hợp cho con trỏ thông minh của bạn
    12. làm cho bạn constructor templated để xử lý xây dựng con trỏ cơ sở từ nguồn gốc.

Và đừng quên bất cứ điều gì tôi có thể đã quên trong danh sách không đầy đủ ở trên.

7

Tôi đã sử dụng boost :: shared_ptr trong vài năm nay và trong khi bạn nói đúng về nhược điểm (không thể chuyển qua con trỏ), tôi nghĩ nó chắc chắn đáng giá vì số lượng lỗi liên quan đến con trỏ nó đã cứu tôi.

Trong công cụ trò chơi homebrew của tôi, tôi đã thay thế các con trỏ bình thường bằng shared_ptr càng nhiều càng tốt. Hiệu suất nhấn nguyên nhân này thực sự không quá tệ nếu bạn đang gọi hầu hết các hàm bằng cách tham chiếu để trình biên dịch không phải tạo quá nhiều cá thể shared_ptr tạm thời.

+1

Ngoài ra, phiên bản tăng của shared_ptr đã di chuyển vào TR1 và do đó cuối cùng sẽ là thư viện chuẩn C++. –

+1

So sánh hiệu suất của các con trỏ thông minh tăng là ở đây: http://www.boost.org/doc/libs/1_39_0/libs/smart_ptr/smarttests.htm –

1

Sự cố với 2 có thể được thực hiện xung quanh. Boost cung cấp tăng :: shared_from_this vì lý do này. Trong thực tế, nó không phải là một vấn đề lớn.

Nhưng lý do chúng đi cùng với tùy chọn # 2 của bạn là nó có thể được sử dụng trong mọi trường hợp. Dựa vào thừa kế không phải lúc nào cũng là một lựa chọn, và sau đó bạn còn lại với một con trỏ thông minh bạn không thể sử dụng cho một nửa mã của bạn.

Tôi phải nói # 2 là tốt nhất, đơn giản vì nó có thể được sử dụng trong mọi trường hợp.

3

Tăng cường cũng có một con trỏ xâm nhập (như giải pháp 1), không yêu cầu thừa kế từ bất kỳ thứ gì. Nó yêu cầu thay đổi con trỏ thành lớp để lưu trữ số tham chiếu và cung cấp các hàm thành viên thích hợp. Tôi đã sử dụng điều này trong trường hợp hiệu quả bộ nhớ là quan trọng, và không muốn chi phí của một đối tượng khác cho mỗi con trỏ dùng chung.

Ví dụ:

class Event { 
public: 
typedef boost::intrusive_ptr<Event> Ptr; 
void addRef(); 
unsigned release(); 
\\ ... 
private: 
unsigned fRefCount; 
}; 

inline void Event::addRef() 
{ 
    fRefCount++; 
} 
inline unsigned Event::release(){ 
    fRefCount--; 
    return fRefCount; 
} 

inline void intrusive_ptr_add_ref(Event* e) 
{ 
    e->addRef(); 
} 

inline void intrusive_ptr_release(Event* e) 
{ 
    if (e->release() == 0) 
    delete e; 
} 

Các PTR typedef được sử dụng để tôi có thể dễ dàng switcth giữa tăng :: shared_ptr <> và đẩy mạnh :: intrusive_ptr <> mà không thay đổi bất kỳ mã khách hàng

3

Nếu bạn dính với những cái nằm trong thư viện chuẩn, bạn sẽ ổn thôi.
Mặc dù có một vài loại khác với các loại bạn đã chỉ định.

  • Shared: Trong trường hợp sở hữu được chia sẻ giữa nhiều đối tượng
  • nước: Trong trường hợp một đối tượng sở hữu đối tượng nhưng chuyển được cho phép.
  • Không thể di chuyển: Nơi một đối tượng sở hữu đối tượng và không thể chuyển đối tượng đó.

Các thư viện chuẩn có:

  • std :: auto_ptr

Boost có một vài chi tiết hơn đã được điều chỉnh bởi tr1 (phiên bản tiếp theo của tiêu chuẩn)

  • std :: tr1 :: shared_ptr
  • std :: tr1 :: weak_ptr

Và những thứ đó vẫn đang tăng (mà tương đối là phải có), hy vọng biến nó thành tr2.

  • boost :: scoped_ptr
  • boost :: scoped_array
  • boost :: shared_array
  • boost :: intrusive_ptr

Xem: Smart Pointers: Or who owns you baby?

+0

Tôi không tin rằng tăng :: scoped_ptr đã biến nó thành tr1, vì vậy nó vẫn tăng :: scoped_ptr, không std :: tr1 :: scoped_ptr. –

+0

Upps. Lỗi của tôi. –

2

Dường như với tôi này câu hỏi giống như hỏi "Thuật toán sắp xếp tốt nhất là gì?" Không có ai trả lời, nó phụ thuộc vào hoàn cảnh của bạn.

Vì mục đích của riêng tôi, tôi đang sử dụng loại của bạn 1. Tôi không có quyền truy cập vào thư viện TR1. Tôi có toàn quyền kiểm soát tất cả các lớp tôi cần phải có con trỏ chia sẻ. Bộ nhớ bổ sung và hiệu quả thời gian của loại 1 có thể là khá nhỏ, nhưng việc sử dụng bộ nhớ và tốc độ là những vấn đề lớn đối với mã của tôi, vì vậy, loại 1 là một dunk slam. Mặt khác, đối với bất cứ ai có thể sử dụng TR1, tôi nghĩ rằng loại 2 std :: tr1 :: shared_ptr class sẽ là một lựa chọn mặc định hợp lý, được sử dụng bất cứ khi nào không có một số lý do nhấn không để dùng nó.

9

Chỉ để cung cấp một cái nhìn khác cho câu trả lời Boost phổ biến (mặc dù đó là câu trả lời đúng cho nhiều sử dụng), hãy xem Loki việc triển khai con trỏ thông minh. Đối với một diễn ngôn về triết lý thiết kế, tác giả gốc của Loki đã viết cuốn sách Modern C++ Design.

+0

+1 kể từ khi tăng không cung cấp tùy chọn deep_copy cho con trỏ thông minh của nó và tôi nghĩ đó là một sự xấu hổ. – n1ckp

1

Dự án của chúng tôi sử dụng con trỏ thông minh rộng rãi. Ban đầu không có sự chắc chắn về con trỏ nào để sử dụng, và do đó một trong những tác giả chính đã chọn một con trỏ xâm nhập trong mô-đun của mình và một con trỏ khác là một phiên bản không xâm phạm.

Nói chung, sự khác biệt giữa hai loại con trỏ không đáng kể. Trường hợp ngoại lệ duy nhất là phiên bản đầu tiên của con trỏ không xâm nhập của chúng tôi mặc nhiên chuyển đổi từ một con trỏ thô và điều này có thể dễ dàng dẫn đến các vấn đề bộ nhớ nếu các con trỏ được sử dụng không đúng cách:

void doSomething (NIPtr<int> const &); 

void foo() { 
    NIPtr<int> i = new int; 
    int & j = *i; 
    doSomething (&j);   // Ooops - owned by two pointers! :(
} 

Một thời gian trước, một số refactoring dẫn đến một số các phần của mã được hợp nhất, và do đó, một sự lựa chọn phải được thực hiện về loại con trỏ nào để sử dụng. Con trỏ không xâm nhập bây giờ đã có hàm khởi tạo chuyển đổi được khai báo là rõ ràng và vì vậy nó đã được quyết định đi với con trỏ xâm nhập để tiết kiệm số lượng thay đổi mã được yêu cầu.

Điều ngạc nhiên lớn nhất của chúng tôi là chúng tôi đã chú ý rằng chúng tôi đã có cải thiện hiệu suất ngay lập tức bằng cách sử dụng con trỏ xâm nhập. Chúng tôi đã không đưa nhiều nghiên cứu vào điều này, và chỉ giả định rằng sự khác biệt là chi phí duy trì đối tượng đếm. Có thể các triển khai khác của con trỏ chia sẻ không xâm phạm đã giải quyết vấn đề này ngay bây giờ.

+0

Đây là lý do tại sao tôi đã bắt đầu typedefing một con trỏ thông minh trong mỗi lớp sẽ được sử dụng với một con trỏ thông minh. Sau đó, quyết định có thể được thực hiện trên cơ sở mỗi lớp và thay đổi mà không ảnh hưởng đến mã khách hàng. Điều này chỉ hoạt động miễn là cả hai loại con trỏ đều có cùng giao diện. – KeithB

1

Những gì bạn đang nói đến là xâm nhậpkhông xâm nhập con trỏ thông minh. Boost có cả hai. boost::intrusive_ptr gọi hàm để giảm và tăng số lượng tham chiếu của đối tượng của bạn, mọi lúc cần thay đổi số tham chiếu. Nó không gọi chức năng thành viên, nhưng chức năng miễn phí. Vì vậy, nó cho phép quản lý các đối tượng mà không cần phải thay đổi định nghĩa của các loại của chúng. Và như bạn nói, boost::shared_ptr không xâm phạm, danh mục của bạn 2.

Tôi có câu trả lời giải thích về intrusive_ptr: Making shared_ptr not use delete. Trong ngắn hạn, bạn sử dụng nó nếu bạn có một đối tượng đã tham chiếu đếm, hoặc cần (như bạn giải thích) một đối tượng đã được tham chiếu để được sở hữu bởi một intrusive_ptr.

Các vấn đề liên quan