C ++ - chuyển tham chiếu tới std :: shared_ptr hoặc boost :: shared_ptr


115

Nếu tôi có một hàm cần làm việc với a shared_ptr, thì sẽ không hiệu quả hơn nếu chuyển nó tham chiếu đến nó (để tránh sao chép shared_ptrđối tượng)? Các tác dụng phụ xấu có thể xảy ra là gì? Tôi hình dung hai trường hợp có thể xảy ra:

1) bên trong hàm, một bản sao được tạo từ đối số, như trong

ClassA::take_copy_of_sp(boost::shared_ptr<foo> &sp)  
{  
     ...  
     m_sp_member=sp; //This will copy the object, incrementing refcount  
     ...  
}  

2) bên trong hàm, đối số chỉ được sử dụng, như trong

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}  

Tôi không thể thấy trong cả hai trường hợp một lý do chính đáng để chuyển boost::shared_ptr<foo>theo giá trị thay vì bằng tham chiếu. Việc chuyển theo giá trị sẽ chỉ "tạm thời" tăng số lượng tham chiếu do sao chép và sau đó giảm nó khi thoát khỏi phạm vi hàm. Tôi có đang nhìn ra cái gì đó không?

Chỉ cần làm rõ, sau khi đọc một số câu trả lời: Tôi hoàn toàn đồng ý về những lo ngại về việc tối ưu hóa quá sớm và tôi luôn cố gắng tìm hiểu sơ lược về các điểm nóng. Câu hỏi của tôi nhiều hơn từ quan điểm mã kỹ thuật thuần túy, nếu bạn hiểu ý tôi.


Tôi không biết liệu bạn có thể sửa đổi các thẻ của câu hỏi của mình hay không, nhưng hãy thử thêm thẻ tăng cường vào đó. Tôi đã cố gắng tìm kiếm câu hỏi này nhưng không thể tìm thấy câu hỏi nào vì tôi đã tìm kiếm thẻ tăng cường và con trỏ thông minh. Vì vậy, tôi đã tìm thấy câu hỏi của bạn ngay sau khi soạn câu hỏi của riêng tôi
Edison Gustavo Muenz

Câu trả lời:


113

Điểm của một shared_ptrcá thể riêng biệt là đảm bảo (càng xa càng tốt) rằng miễn là nó shared_ptrnằ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_ptrvớ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 đó:

  1. 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.

  2. 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::stringbá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_ptrcho 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_messageshared_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_messagecố 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ì msgnó 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_ptrviệ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_ptrkhô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_ptrthà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::stringxuyê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.


16
Shared_ptr được chuyển vào đã tồn tại trong một phạm vi, tại trang web cuộc gọi. Bạn có thể tạo ra một kịch bản phức tạp trong đó mã trong câu hỏi này sẽ bị nổ do con trỏ treo lơ lửng, nhưng sau đó tôi cho rằng bạn gặp vấn đề lớn hơn tham số tham chiếu!
Magnus Hoff

10
Nó có thể được lưu trữ trong một thành viên. Bạn có thể gọi một cái gì đó xảy ra để xóa thành viên đó. Toàn bộ điểm của smart_ptr là tránh phải điều phối thời gian tồn tại trong phân cấp hoặc phạm vi lồng nhau xung quanh ngăn xếp cuộc gọi, vì vậy đó là lý do tại sao tốt nhất là giả định rằng các vòng đời không làm điều đó trong các chương trình như vậy.
Daniel Earwicker

7
Đó không thực sự là quan điểm của tôi! Nếu bạn nghĩ rằng những gì tôi đang nói là một cái gì đó cụ thể để làm với mã của tôi, bạn có thể chưa hiểu tôi. Tôi đang nói về một ngụ ý không thể tránh khỏi về lý do shared_ptr tồn tại ngay từ đầu: nhiều vòng đời đối tượng không chỉ đơn giản liên quan đến các lời gọi hàm.
Daniel Earwicker

8
@DanielEarwicker hoàn toàn đồng ý với tất cả các quan điểm của bạn và ngạc nhiên về mức độ phản đối. Điều gì đó làm cho mối quan tâm của bạn thậm chí có liên quan hơn là phân luồng, khi điều này trở nên liên quan, đảm bảo về tính hợp lệ của đối tượng trở nên quan trọng hơn nhiều. Câu trả lời tốt.
radman

3
Cách đây không lâu, tôi đã tìm ra một lỗi rất nghiêm trọng do chuyển tham chiếu đến con trỏ được chia sẻ. Mã đang xử lý sự thay đổi trạng thái của một đối tượng và khi nó nhận thấy trạng thái của đối tượng đã thay đổi, nó sẽ xóa nó khỏi bộ sưu tập các đối tượng ở trạng thái trước đó và chuyển nó vào bộ sưu tập các đối tượng ở trạng thái mới. Thao tác loại bỏ đã phá hủy con trỏ được chia sẻ cuối cùng tới đối tượng. Hàm thành viên đã được gọi trên một tham chiếu đến con trỏ được chia sẻ trong bộ sưu tập. Bùng nổ. Daniel Earwicker đã đúng.
David Schwartz

115

Tôi thấy mình không đồng ý với câu trả lời được bình chọn cao nhất, vì vậy tôi đã đi tìm chuyên gia về thuốc phiện và chúng đây rồi. Từ http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything

Herb Sutter: "khi bạn vượt qua shared_ptrs, các bản sao sẽ đắt"

Scott Meyers: "Không có gì đặc biệt về shared_ptr khi nói đến việc bạn chuyển nó theo giá trị hay chuyển nó theo tham chiếu. Sử dụng chính xác phân tích mà bạn sử dụng cho bất kỳ loại người dùng xác định nào khác. Mọi người dường như có nhận thức rằng shared_ptr bằng cách nào đó giải quyết được tất cả các vấn đề về quản lý và vì nó nhỏ nên không nhất thiết phải rẻ để chuyển theo giá trị. Nó phải được sao chép và có một chi phí liên quan đến điều đó ... rất đắt để chuyển nó theo giá trị, vì vậy nếu tôi có thể bỏ qua nó có ngữ nghĩa thích hợp trong chương trình của tôi, tôi sẽ chuyển nó bằng cách tham chiếu đến const hoặc tham chiếu thay thế "

Herb Sutter: "luôn luôn chuyển chúng bằng tham chiếu đến const, và đôi khi có thể vì bạn biết những gì bạn đã gọi có thể sửa đổi thứ mà bạn nhận được tham chiếu, có thể sau đó bạn có thể chuyển theo giá trị ... nếu bạn sao chép chúng dưới dạng tham số, oh Chúa ơi, bạn hầu như không bao giờ cần phải tăng số lượng tham chiếu đó vì dù sao nó vẫn đang được giữ sống, và bạn nên chuyển nó bằng cách tham chiếu, vì vậy hãy làm điều đó "

Cập nhật: Herb đã mở rộng về điều này ở đây: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ , mặc dù đạo đức của câu chuyện là bạn không nên bỏ qua shared_ptrs '' trừ khi bạn muốn sử dụng hoặc thao tác chính con trỏ thông minh, chẳng hạn như để chia sẻ hoặc chuyển quyền sở hữu. "


8
Rất vui! Thật tuyệt khi thấy hai trong số những chuyên gia hàng đầu về chủ đề này đã công khai bác bỏ những hiểu biết thông thường về SO.
Stephan Tolksdorf

3
"Không có gì đặc biệt về shared_ptr khi nói đến việc bạn chuyển nó theo giá trị hay chuyển nó theo tham chiếu" - Tôi thực sự không đồng ý với điều này. Nó là đặc biệt. Cá nhân tôi muốn chơi nó an toàn, và có một chút hiệu suất. Nếu có một vùng mã cụ thể mà tôi cần tối ưu hóa thì chắc chắn, tôi sẽ xem xét các lợi ích về hiệu suất của shared_ptr pass by const ref.
JasonZ

3
Cũng rất thú vị khi lưu ý rằng, mặc dù có thỏa thuận về việc lạm dụng shared_ptrs, nhưng không có thỏa thuận về vấn đề chuyển-theo-giá-trị-so-tham chiếu.
Nicol Bolas

2
Scott Meyers: "vì vậy nếu tôi có thể xử lý nó với ngữ nghĩa thích hợp trong chương trình của tôi ..." tức là không mâu thuẫn với câu trả lời của tôi, điều này chỉ ra rằng việc tìm hiểu xem việc thay đổi các tham số const &sẽ ảnh hưởng đến ngữ nghĩa chỉ dễ dàng trong rất các chương trình đơn giản.
Daniel Earwicker

7
Herb Sutter: "Đôi khi có thể vì bạn biết thứ bạn gọi có thể sửa đổi thứ mà bạn tham khảo từ đó". Một lần nữa, điều đó được miễn trừ cho một trường hợp nhỏ, vì vậy nó không mâu thuẫn với câu trả lời của tôi. Câu hỏi vẫn còn là: làm thế nào để bạn biết nó an toàn khi sử dụng một ref ref? Rất dễ dàng để chứng minh trong một chương trình đơn giản, không quá dễ dàng trong một chương trình phức tạp. Nhưng này, đây C ++, và vì vậy chúng tôi ưu tiên tối ưu hóa vi mô sớm hơn hầu hết các mối quan tâm kỹ thuật khác, phải không ?! :)
Daniel Earwicker

22

Tôi khuyên bạn không nên áp dụng phương pháp này trừ khi bạn và các lập trình viên khác mà bạn làm việc thực sự, thực sự biết tất cả những gì bạn đang làm.

Đầu tiên, bạn không biết giao diện lớp của mình có thể phát triển như thế nào và bạn muốn ngăn các lập trình viên khác làm điều xấu. Việc chuyển một shared_ptr theo tham chiếu không phải là điều mà một lập trình viên nên mong đợi để xem, bởi vì nó không phải là thành ngữ, và điều đó khiến bạn dễ dàng sử dụng nó không chính xác. Chương trình phòng thủ: làm cho giao diện khó sử dụng không chính xác. Vượt qua bằng cách tham khảo sẽ chỉ đưa ra các vấn đề sau này.

Thứ hai, đừng tối ưu hóa cho đến khi bạn biết rằng lớp cụ thể này sẽ là một vấn đề. Hồ sơ trước, và sau đó nếu chương trình của bạn thực sự cần sự thúc đẩy bằng cách chuyển qua tham chiếu, thì có thể. Nếu không, đừng đổ mồ hôi cho những thứ nhỏ nhặt (tức là thêm N lệnh cần thiết để chuyển theo giá trị) thay vào đó hãy lo lắng về thiết kế, cấu trúc dữ liệu, thuật toán và khả năng bảo trì lâu dài.


Mặc dù câu trả lời của litb là đúng về mặt kỹ thuật, nhưng đừng bao giờ đánh giá thấp sự “lười biếng” của các lập trình viên (tôi cũng lười!). Câu trả lời của littlenag thì tốt hơn, rằng tham chiếu đến shared_ptr sẽ không mong muốn và có thể (có thể) là một tối ưu hóa không cần thiết khiến cho việc bảo trì trong tương lai trở nên khó khăn hơn.
netjeff

18

Có, tham khảo là tốt ở đó. Bạn không có ý định trao quyền sở hữu chung cho phương thức; nó chỉ muốn làm việc với nó. Bạn cũng có thể tham khảo cho trường hợp đầu tiên, vì bạn vẫn sao chép nó. Nhưng đối với trường hợp đầu tiên, nó quyền sở hữu. Có một thủ thuật này để vẫn sao chép nó một lần duy nhất:

void ClassA::take_copy_of_sp(boost::shared_ptr<foo> sp) {
    m_sp_member.swap(sp);
}

Bạn cũng nên sao chép khi bạn trả lại nó (tức là không trả lại một tham chiếu). Bởi vì lớp của bạn không biết khách hàng đang làm gì với nó (nó có thể lưu một con trỏ tới nó và sau đó vụ nổ lớn xảy ra). Nếu sau đó nó cho thấy đó là một nút cổ chai (hồ sơ đầu tiên!), Thì bạn vẫn có thể trả về một tham chiếu.


Chỉnh sửa : Tất nhiên, như những người khác chỉ ra, điều này chỉ đúng nếu bạn biết mã của mình và biết rằng bạn không đặt lại con trỏ được chia sẻ đã chuyển theo một cách nào đó. Nếu nghi ngờ, chỉ cần chuyển qua giá trị.


11

Nó là hợp lý để vượt qua shared_ptrbằng cách const&. Nó sẽ không có khả năng gây ra rắc rối (ngoại trừ trường hợp không chắc chắn rằng tham chiếu shared_ptrđược tham chiếu bị xóa trong khi gọi hàm, được Earwicker trình bày chi tiết) và có thể sẽ nhanh hơn nếu bạn vượt qua nhiều điều này. Nhớ lại; mặc định boost::shared_ptrlà an toàn luồng, vì vậy việc sao chép nó bao gồm một gia số an toàn cho luồng.

Cố gắng sử dụng const&thay vì chỉ &vì các đối tượng tạm thời có thể không được chuyển bằng tham chiếu không phải const. (Mặc dù phần mở rộng ngôn ngữ trong MSVC vẫn cho phép bạn làm điều đó)


3
Có, tôi luôn sử dụng tham chiếu const, tôi chỉ quên đưa nó vào ví dụ của tôi. Dù sao, MSVC cho phép liên kết các tham chiếu không phải const với các khoảng thời gian tạm thời không phải do lỗi, nhưng vì theo mặc định, nó có thuộc tính "C / C ++ -> Language -> Disable Language Extension" được đặt thành "NO". Kích hoạt nó và nó sẽ không biên dịch chúng ...
abigagli

abigagli: Nghiêm túc chứ? Ngọt! Tôi sẽ thi hành này tại nơi làm việc, điều đầu tiên vào ngày mai;)
Magnus Hoff

10

Trong trường hợp thứ hai, làm điều này đơn giản hơn:

Class::only_work_with_sp(foo &sp)
{    
    ...  
    sp.do_something();  
    ...  
}

Bạn có thể gọi nó là

only_work_with_sp(*sp);

3
Nếu bạn áp dụng quy ước để sử dụng các tham chiếu đối tượng khi bạn không cần lấy một bản sao của con trỏ, nó cũng dùng để ghi lại ý định của bạn. Nó cũng cho bạn cơ hội sử dụng tham chiếu const.
Mark Ransom

Có, tôi đồng ý về việc sử dụng các tham chiếu đến các đối tượng như một phương tiện để thể hiện rằng hàm được gọi không "nhớ" bất cứ điều gì về đối tượng đó. Thông thường tôi sử dụng lập luận chính thức con trỏ nếu hàm được "theo dõi" của đối tượng
abigagli

3

Tôi sẽ tránh một tham chiếu "thuần túy" trừ khi hàm có thể sửa đổi con trỏ một cách rõ ràng.

A const &có thể là một tối ưu hóa vi mô hợp lý khi gọi các chức năng nhỏ - ví dụ: để cho phép tối ưu hóa hơn nữa, như nội tuyến hóa một số điều kiện. Ngoài ra, tăng / giảm - vì nó an toàn cho chuỗi - là một điểm đồng bộ hóa. Tuy nhiên, tôi sẽ không mong đợi điều này tạo ra sự khác biệt lớn trong hầu hết các tình huống.

Nói chung, bạn nên sử dụng phong cách đơn giản hơn trừ khi bạn có lý do để không. Sau đó, sử dụng một const &cách nhất quán hoặc thêm nhận xét tại sao nếu bạn chỉ sử dụng nó ở một vài nơi.


3

Tôi sẽ ủng hộ việc truyền con trỏ được chia sẻ bằng cách tham chiếu const - một ngữ nghĩa mà hàm được truyền với con trỏ KHÔNG sở hữu con trỏ, đây là một thành ngữ rõ ràng cho các nhà phát triển.

Cạm bẫy duy nhất là trong nhiều chương trình luồng, đối tượng được trỏ bởi con trỏ chia sẻ sẽ bị phá hủy trong một luồng khác. Vì vậy, có thể nói việc sử dụng tham chiếu const của con trỏ chia sẻ là an toàn trong chương trình đơn luồng.

Việc chuyển con trỏ chia sẻ bằng tham chiếu không phải const đôi khi rất nguy hiểm - lý do là các chức năng hoán đổi và đặt lại mà hàm có thể gọi bên trong để phá hủy đối tượng vẫn được coi là hợp lệ sau khi hàm trả về.

Tôi đoán nó không phải là về việc tối ưu hóa quá sớm - nó là về việc tránh lãng phí chu kỳ CPU không cần thiết khi bạn đã rõ mình muốn làm gì và thành ngữ mã hóa đã được các nhà phát triển đồng nghiệp của bạn chấp nhận một cách chắc chắn.

Chỉ 2 xu của tôi :-)


1
Xem nhận xét ở trên của David Schwartz "... Tôi đã tìm ra một lỗi rất nghiêm trọng do chuyển tham chiếu đến con trỏ được chia sẻ. Đoạn mã đang xử lý sự thay đổi trạng thái của đối tượng và khi nó nhận thấy trạng thái của đối tượng đã thay đổi, nó đã xóa nó khỏi bộ sưu tập các đối tượng ở trạng thái trước đó và chuyển nó vào bộ sưu tập các đối tượng ở trạng thái mới. Thao tác remove đã phá hủy con trỏ được chia sẻ cuối cùng tới đối tượng. Hàm thành viên đã được gọi trên một tham chiếu đến con trỏ được chia sẻ trong bộ sưu tập. Bùng nổ ... "
Jason Harrison.

3

Có vẻ như tất cả những ưu và nhược điểm ở đây thực sự có thể được tổng quát thành BẤT KỲ loại nào được chuyển qua tham chiếu chứ không chỉ shared_ptr. Theo tôi, bạn nên biết ngữ nghĩa của việc truyền qua tham chiếu, tham chiếu const và giá trị và sử dụng nó một cách chính xác. Nhưng hoàn toàn không có gì sai khi chuyển shared_ptr bằng tham chiếu, trừ khi bạn nghĩ rằng tất cả các tham chiếu đều xấu ...

Để quay lại ví dụ:

Class::only_work_with_sp( foo &sp ) //Again, no copy here  
{    
    ...  
    sp.do_something();  
    ...  
}

Làm sao bạn biết điều đó sp.do_something() sẽ không bị nổ do một con trỏ treo lơ lửng?

Sự thật là, shared_ptr hay không, const hay không, điều này có thể xảy ra nếu bạn có lỗi thiết kế, như chia sẻ trực tiếp hoặc gián tiếp quyền sở hữu spgiữa các luồng, bỏ lỡ một đối tượng delete this, bạn có quyền sở hữu vòng tròn hoặc các lỗi quyền sở hữu khác.


2

Một điều mà tôi chưa thấy đề cập là khi bạn chuyển con trỏ chia sẻ bằng tham chiếu, bạn sẽ mất chuyển đổi ngầm định mà bạn nhận được nếu bạn muốn chuyển con trỏ chia sẻ lớp dẫn xuất thông qua tham chiếu đến con trỏ chia sẻ lớp cơ sở.

Ví dụ: mã này sẽ tạo ra một lỗi, nhưng nó sẽ hoạt động nếu bạn thay đổi test()để con trỏ được chia sẻ không được chuyển qua tham chiếu.

#include <boost/shared_ptr.hpp>

class Base { };
class Derived: public Base { };

// ONLY instances of Base can be passed by reference.  If you have a shared_ptr
// to a derived type, you have to cast it manually.  If you remove the reference
// and pass the shared_ptr by value, then the cast is implicit so you don't have
// to worry about it.
void test(boost::shared_ptr<Base>& b)
{
    return;
}

int main(void)
{
    boost::shared_ptr<Derived> d(new Derived);
    test(d);

    // If you want the above call to work with references, you will have to manually cast
    // pointers like this, EVERY time you call the function.  Since you are creating a new
    // shared pointer, you lose the benefit of passing by reference.
    boost::shared_ptr<Base> b = boost::dynamic_pointer_cast<Base>(d);
    test(b);

    return 0;
}

1

Tôi giả sử rằng bạn đã quen với việc tối ưu hóa sớm và đang hỏi điều này vì mục đích học thuật hoặc vì bạn đã tách biệt một số mã tồn tại từ trước có hiệu suất kém.

Qua tham khảo là được

Truyền qua tham chiếu const thì tốt hơn và thường có thể được sử dụng, vì nó không ép buộc const-ness lên đối tượng được trỏ tới.

Bạn không có nguy cơ bị mất con trỏ do sử dụng tham chiếu. Tham chiếu đó là bằng chứng cho thấy bạn có một bản sao của con trỏ thông minh trước đó trong ngăn xếp và chỉ một luồng sở hữu ngăn xếp cuộc gọi, do đó, bản sao tồn tại trước đó sẽ không biến mất.

Sử dụng tài liệu tham khảo thường hiệu quả hơn vì những lý do bạn đề cập, nhưng không được đảm bảo . Hãy nhớ rằng việc bỏ qua một đối tượng cũng có thể mất công. Kịch bản sử dụng tham chiếu lý tưởng của bạn sẽ là nếu kiểu mã hóa của bạn liên quan đến nhiều hàm nhỏ, trong đó con trỏ sẽ được chuyển từ hàm này sang hàm khác trước khi được sử dụng.

Bạn nên luôn tránh lưu trữ con trỏ thông minh của mình làm tài liệu tham khảo. Class::take_copy_of_sp(&sp)Ví dụ của bạn cho thấy cách sử dụng chính xác cho điều đó.


1
"Bạn không có nguy cơ mất con trỏ do sử dụng một tham chiếu. Tham chiếu đó là bằng chứng cho thấy bạn có một bản sao của con trỏ thông minh trước đó trong ngăn xếp" Hay một thành viên dữ liệu ...?
Daniel Earwicker

Hãy xem xét sự kỳ diệu của boost :: thread và boost :: ref: boost :: function <int> functionPointer = boost :: bind (doSomething, boost :: ref (sharedPtrInstance)); m_workerThread = new boost :: thread (functionPointer); ... xóa sharedPtrInstance
Jason Harrison

1

Giả sử chúng tôi không quan tâm đến độ đúng của hằng số (hoặc hơn thế nữa, ý bạn là cho phép các hàm có thể sửa đổi hoặc chia sẻ quyền sở hữu dữ liệu được chuyển vào), việc chuyển một boost :: shared_ptr theo giá trị sẽ an toàn hơn chuyển nó theo tham chiếu như chúng tôi cho phép boost gốc :: shared_ptr kiểm soát thời gian tồn tại của chính nó. Xem xét kết quả của đoạn mã sau ...

void FooTakesReference( boost::shared_ptr< int > & ptr )
{
    ptr.reset(); // We reset, and so does sharedA, memory is deleted.
}

void FooTakesValue( boost::shared_ptr< int > ptr )
{
    ptr.reset(); // Our temporary is reset, however sharedB hasn't.
}

void main()
{
    boost::shared_ptr< int > sharedA( new int( 13 ) );
    boost::shared_ptr< int > sharedB( new int( 14 ) );

    FooTakesReference( sharedA );

    FooTakesValue( sharedB );
}

Từ ví dụ trên, chúng ta thấy rằng việc chuyển sharedA theo tham chiếu cho phép FooTakesReference đặt lại con trỏ ban đầu, điều này làm giảm số lần sử dụng của nó xuống 0, phá hủy dữ liệu của nó. Tuy nhiên, FooTakesValue không thể đặt lại con trỏ ban đầu, đảm bảo dữ liệu của sharedB vẫn có thể sử dụng được. Khi một nhà phát triển khác chắc chắn đến cùng và cố gắng cõng sự tồn tại mong manh của sharedA , sự hỗn loạn xảy ra. Tuy nhiên, nhà phát triển sharedB may mắn về nhà sớm vì mọi thứ vẫn ổn trong thế giới của anh ta.

Trong trường hợp này, độ an toàn của mã cao hơn nhiều so với bất kỳ quá trình sao chép cải thiện tốc độ nào được tạo ra. Đồng thời, boost :: shared_ptr nhằm cải thiện độ an toàn của mã. Sẽ dễ dàng hơn nhiều để chuyển từ một bản sao sang một tài liệu tham khảo, nếu thứ gì đó yêu cầu loại tối ưu hóa thích hợp này.


1

Sandy đã viết: "Có vẻ như tất cả những ưu và nhược điểm ở đây thực sự có thể được tổng quát hóa thành BẤT KỲ loại nào được chuyển qua tham chiếu không chỉ shared_ptr."

Đúng ở một mức độ nào đó, nhưng quan điểm của việc sử dụng shared_ptr là để loại bỏ các mối quan tâm về vòng đời của đối tượng và để trình biên dịch xử lý điều đó cho bạn. Nếu bạn định chuyển một con trỏ được chia sẻ theo tham chiếu và cho phép các ứng dụng khách của phương thức non-const của đối tượng được đếm-tham chiếu của bạn gọi phương thức có thể giải phóng dữ liệu đối tượng, thì việc sử dụng con trỏ được chia sẻ gần như vô nghĩa.

Tôi đã viết "gần như" trong câu trước đó bởi vì hiệu suất có thể là một mối quan tâm và nó 'có thể' được biện minh trong một số trường hợp hiếm hoi, nhưng tôi cũng sẽ tự tránh trường hợp này và tự mình tìm kiếm tất cả các giải pháp tối ưu hóa có thể có khác, chẳng hạn như xem xét nghiêm túc khi thêm một cấp độ khác của hướng dẫn, đánh giá lười biếng, v.v.

Mã tồn tại trước đó là tác giả, hoặc thậm chí đăng bộ nhớ của tác giả, yêu cầu các giả định ngầm về hành vi, đặc biệt là hành vi về vòng đời của đối tượng, yêu cầu tài liệu rõ ràng, ngắn gọn, dễ đọc và sau đó nhiều khách hàng sẽ không đọc nó! Sự đơn giản hầu như luôn vượt trội hiệu quả và hầu như luôn có những cách khác để trở nên hiệu quả. Nếu bạn thực sự cần chuyển các giá trị bằng tham chiếu để tránh sao chép sâu bởi các hàm tạo bản sao của đối tượng được đếm tham chiếu của bạn (và toán tử bằng), thì có lẽ bạn nên xem xét các cách để làm cho dữ liệu được sao chép sâu trở thành con trỏ được đếm tham chiếu có thể sao chép nhanh chóng. (Tất nhiên, đó chỉ là một kịch bản thiết kế có thể không áp dụng cho tình huống của bạn).


1

Tôi đã từng làm việc trong một dự án có nguyên tắc rất mạnh mẽ về việc vượt qua các con trỏ thông minh theo giá trị. Khi tôi được yêu cầu thực hiện một số phân tích hiệu suất - tôi nhận thấy rằng để tăng và giảm bộ đếm tham chiếu của con trỏ thông minh, ứng dụng dành từ 4-6% thời gian sử dụng của bộ xử lý.

Nếu bạn muốn vượt qua các gợi ý thông minh theo giá trị chỉ để tránh gặp vấn đề trong những trường hợp kỳ lạ như được mô tả từ Daniel Earwicker, hãy đảm bảo rằng bạn hiểu cái giá mà bạn phải trả cho nó.

Nếu bạn quyết định sử dụng tham chiếu, lý do chính để sử dụng tham chiếu const là để làm cho nó có thể có dự báo ngầm khi bạn cần chuyển con trỏ chia sẻ đến đối tượng từ lớp kế thừa lớp bạn sử dụng trong giao diện.


0

Ngoài những gì litb đã nói, tôi muốn chỉ ra rằng nó có thể chuyển bằng tham chiếu const trong ví dụ thứ hai, theo cách đó bạn chắc chắn rằng mình không vô tình sửa đổi nó.


0
struct A {
  shared_ptr<Message> msg;
  shared_ptr<Message> * ptr_msg;
}
  1. chuyển theo giá trị:

    void set(shared_ptr<Message> msg) {
      this->msg = msg; /// create a new shared_ptr, reference count will be added;
    } /// out of method, new created shared_ptr will be deleted, of course, reference count also be reduced;
  2. chuyển qua tham khảo:

    void set(shared_ptr<Message>& msg) {
     this->msg = msg; /// reference count will be added, because reference is just an alias.
     }
  3. đi qua con trỏ:

    void set(shared_ptr<Message>* msg) {
      this->ptr_msg = msg; /// reference count will not be added;
    }

0

Mỗi đoạn mã phải mang một số ý nghĩa. Nếu bạn chuyển một con trỏ được chia sẻ theo giá trị ở mọi nơi trong ứng dụng, điều này có nghĩa là "Tôi không chắc về những gì đang xảy ra ở nơi khác, do đó tôi ủng hộ sự an toàn thô ". Đây không phải là những gì tôi gọi là một dấu hiệu tin cậy tốt cho các lập trình viên khác, những người có thể tham khảo mã.

Dù sao, ngay cả khi một hàm nhận được tham chiếu const và bạn "không chắc", bạn vẫn có thể tạo một bản sao của con trỏ dùng chung ở đầu hàm, để thêm một tham chiếu mạnh vào con trỏ. Đây cũng có thể được coi là một gợi ý về thiết kế ("con trỏ có thể được sửa đổi ở nơi khác").

Vì vậy, có, IMO, mặc định phải là " truyền qua tham chiếu const ".

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.