Điểm của một shared_ptr
cá thể riêng biệt là đảm bảo (càng xa càng tốt) rằng miễn là nó shared_ptr
nằm trong phạm vi, đối tượng mà nó trỏ tới sẽ vẫn tồn tại, vì số lượng tham chiếu của nó sẽ ít nhất là 1.
Class::only_work_with_sp(boost::shared_ptr<foo> sp)
{
// sp points to an object that cannot be destroyed during this function
}
Vì vậy, bằng cách sử dụng tham chiếu đến a shared_ptr
, bạn vô hiệu hóa đảm bảo đó. Vì vậy, trong trường hợp thứ hai của bạn:
Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here
{
...
sp->do_something();
...
}
Làm thế nào để bạn biết rằng nó sp->do_something()
sẽ không bị nổ do một con trỏ rỗng?
Tất cả phụ thuộc vào những gì có trong các phần '...' của mã. Điều gì sẽ xảy ra nếu bạn gọi một cái gì đó trong dấu '...' đầu tiên có tác dụng phụ (ở đâu đó trong phần khác của mã) là xóa một shared_ptr
đối tượng đó? Và điều gì sẽ xảy ra nếu nó là vật duy nhất còn lại khác biệt shared_ptr
với đối tượng đó? Bye bye đối tượng, chỉ nơi bạn sắp thử và sử dụng nó.
Vì vậy, có hai cách để trả lời câu hỏi đó:
Kiểm tra nguồn của toàn bộ chương trình của bạn rất cẩn thận cho đến khi bạn chắc chắn rằng đối tượng sẽ không chết trong phần thân hàm.
Thay đổi tham số trở lại thành một đối tượng riêng biệt thay vì một tham chiếu.
Một chút lời khuyên chung áp dụng ở đây: đừng bận tâm đến việc thực hiện các thay đổi rủi ro đối với mã của bạn vì lợi ích của hiệu suất cho đến khi bạn định thời gian sản phẩm của mình trong một tình huống thực tế trong một hồ sơ và được kết luận rằng thay đổi bạn muốn thực hiện sẽ tạo ra sự khác biệt đáng kể đối với hiệu suất.
Cập nhật cho người bình luận JQ
Đây là một ví dụ giả định. Nó cố tình đơn giản, vì vậy sai lầm sẽ rõ ràng. Trong các ví dụ thực tế, sai lầm không quá rõ ràng vì nó được ẩn trong các lớp chi tiết thực.
Chúng tôi có một chức năng sẽ gửi một tin nhắn ở đâu đó. Nó có thể là một thông báo lớn, vì vậy thay vì sử dụng một thông std::string
báo có khả năng bị sao chép khi nó được chuyển đến nhiều nơi, chúng tôi sử dụng a shared_ptr
cho một chuỗi:
void send_message(std::shared_ptr<std::string> msg)
{
std::cout << (*msg.get()) << std::endl;
}
(Chúng tôi chỉ "gửi" nó đến bảng điều khiển cho ví dụ này).
Bây giờ chúng ta muốn thêm một cơ sở để ghi nhớ tin nhắn trước đó. Chúng tôi muốn hành vi sau: một biến phải tồn tại có chứa thông báo được gửi gần đây nhất, nhưng trong khi một thông báo hiện đang được gửi đi thì không được có thông báo trước đó (biến phải được đặt lại trước khi gửi). Vì vậy, chúng tôi khai báo biến mới:
std::shared_ptr<std::string> previous_message;
Sau đó, chúng tôi sửa đổi chức năng của mình theo các quy tắc mà chúng tôi đã chỉ định:
void send_message(std::shared_ptr<std::string> msg)
{
previous_message = 0;
std::cout << *msg << std::endl;
previous_message = msg;
}
Vì vậy, trước khi bắt đầu gửi, chúng tôi hủy tin nhắn hiện tại trước đó và sau khi gửi xong, chúng tôi có thể lưu trữ tin nhắn mới trước đó. Tất cả đều tốt. Đây là một số mã kiểm tra:
send_message(std::shared_ptr<std::string>(new std::string("Hi")));
send_message(previous_message);
Và đúng như dự đoán, bản này in Hi!
hai lần.
Bây giờ cùng với Mr Maintainer, người nhìn vào mã và nghĩ: Này, tham số đó send_message
là shared_ptr
:
void send_message(std::shared_ptr<std::string> msg)
Rõ ràng điều đó có thể được thay đổi thành:
void send_message(const std::shared_ptr<std::string> &msg)
Hãy nghĩ đến việc nâng cao hiệu suất mà điều này sẽ mang lại! (Đừng bận tâm rằng chúng tôi sắp gửi một tin nhắn thường lớn qua một số kênh, vì vậy việc nâng cao hiệu suất sẽ rất nhỏ đến mức không thể đo lường được).
Nhưng vấn đề thực sự là bây giờ mã kiểm tra sẽ hiển thị hành vi không xác định (trong các bản dựng gỡ lỗi Visual C ++ 2010, nó bị treo).
Mr Maintainer ngạc nhiên vì điều này, nhưng thêm một biện pháp phòng thủ để send_message
cố gắng ngăn chặn sự cố xảy ra:
void send_message(const std::shared_ptr<std::string> &msg)
{
if (msg == 0)
return;
Nhưng tất nhiên nó vẫn tiếp tục và gặp sự cố, bởi vì msg
nó không bao giờ vô hiệu khisend_message
được gọi.
Như tôi đã nói, với tất cả các mã quá gần nhau trong một ví dụ nhỏ, thật dễ dàng để tìm ra lỗi. Nhưng trong các chương trình thực, với các mối quan hệ phức tạp hơn giữa các đối tượng có thể thay đổi giữ các con trỏ với nhau, rất dễ khiến những sai lầm, và khó có thể xây dựng các trường hợp thử nghiệm cần thiết để phát hiện các sai lầm.
Giải pháp dễ dàng, trong đó bạn muốn một hàm có thể dựa vào shared_ptr
việc tiếp tục không có giá trị trong suốt, là để hàm phân bổ giá trị true của chính nó shared_ptr
, thay vì dựa vào tham chiếu đến giá trị hiện có shared_ptr
.
Nhược điểm là sao chép a shared_ptr
không miễn phí: ngay cả các triển khai "không khóa" cũng phải sử dụng một hoạt động được lồng vào nhau để đáp ứng các đảm bảo về luồng. Vì vậy, có thể có những tình huống mà một chương trình có thể được tăng tốc đáng kể bằng cách thay đổi a shared_ptr
thành a shared_ptr &
. Nhưng đây không phải là một thay đổi có thể được thực hiện một cách an toàn cho tất cả các chương trình. Nó thay đổi ý nghĩa logic của chương trình.
Lưu ý rằng một lỗi tương tự sẽ xảy ra nếu chúng tôi sử dụng std::string
xuyên suốt thay vì std::shared_ptr<std::string>
và thay vì:
previous_message = 0;
để xóa tin nhắn, chúng tôi đã nói:
previous_message.clear();
Sau đó, triệu chứng sẽ là tình cờ gửi một tin nhắn trống, thay vì hành vi không xác định. Chi phí cho một bản sao bổ sung của một chuỗi rất lớn có thể đáng kể hơn nhiều so với chi phí sao chép a shared_ptr
, vì vậy sự đánh đổi có thể khác.