Chuyển con trỏ được chia sẻ làm đối số


88

Nếu tôi khai báo một đối tượng được bao bọc trong một con trỏ dùng chung:

std::shared_ptr<myClass> myClassObject(new myClass());

thì tôi muốn chuyển nó làm đối số cho một phương thức:

DoSomething(myClassObject);

//the called method
void DoSomething(std::shared_ptr<myClass> arg1)
{
   arg1->someField = 4;
}

Điều trên có chỉ đơn giản là tăng số lượng tham chiếu của shared_pt và mọi thứ đều tuyệt không? Hay nó để lại một con trỏ lơ lửng?

Bạn vẫn phải làm điều này ?:

DoSomething(myClassObject.Get());

void DoSomething(std::shared_ptr<myClass>* arg1)
{
   (*arg1)->someField = 4;
}

Tôi nghĩ rằng cách thứ 2 có thể hiệu quả hơn vì nó chỉ phải sao chép 1 địa chỉ (trái ngược với toàn bộ con trỏ thông minh), nhưng cách thứ nhất có vẻ dễ đọc hơn và tôi không dự đoán sẽ đẩy giới hạn hiệu suất. Tôi chỉ muốn đảm bảo rằng không có điều gì nguy hiểm về nó.

Cảm ơn bạn.


14
const std::shared_ptr<myClass>& arg1
Captain Obvlious

3
Cách thứ hai bị hỏng, cách thứ nhất là thành ngữ nếu bạn thực sự cần chức năng của mình để chia sẻ quyền sở hữu. Nhưng, có DoSomethingthực sự cần chia sẻ quyền sở hữu không? Có vẻ như nó chỉ nên lấy một tham chiếu thay thế ...
ildjarn

8
@SteveH: Không, nhưng tại sao hàm của bạn phải buộc các ngữ nghĩa quyền sở hữu đối tượng đặc biệt đối với người gọi của nó nếu nó thực sự không cần chúng? Chức năng của bạn không làm gì có thể tốt hơn void DoSomething(myClass& arg1).
ildjarn

2
@SteveH: Toàn bộ mục đích của con trỏ thông minh là để xử lý các vấn đề về quyền sở hữu đối tượng thông thường - nếu bạn không có những vấn đề đó, ngay từ đầu bạn không nên sử dụng con trỏ thông minh. ; -] shared_ptr<>Cụ thể, bạn phải chuyển theo giá trị để thực sự chia sẻ quyền sở hữu.
ildjarn

4
Không liên quan: nói chung, std::make_sharedkhông chỉ hiệu quả hơn mà còn an toàn hơn so với hàm std::shared_ptrtạo.
R. Martinho Fernandes,

Câu trả lời:


171

Tôi muốn chuyển một con trỏ dùng chung cho một hàm. Bạn có thể giúp tôi với đó?

Chắc chắn, tôi có thể giúp bạn điều đó. Tôi giả sử bạn có một số hiểu biết về ngữ nghĩa quyền sở hữu trong C ++. Có đúng như vậy không?

Vâng, tôi hoàn toàn thoải mái với chủ đề này.

Tốt.

Được rồi, tôi chỉ có thể nghĩ ra hai lý do để shared_ptrtranh luận:

  1. Hàm muốn chia sẻ quyền sở hữu đối tượng;
  2. Hàm thực hiện một số thao tác hoạt động cụ thể trên shared_ptrs.

Bạn quan tâm đến cái nào?

Tôi đang tìm kiếm một câu trả lời chung, vì vậy tôi thực sự quan tâm đến cả hai. Tuy nhiên, tôi tò mò về ý của bạn trong trường hợp số 2.

Ví dụ về các hàm như vậy bao gồm std::static_pointer_cast, bộ so sánh tùy chỉnh hoặc vị từ. Ví dụ: nếu bạn cần tìm tất cả shared_ptr duy nhất từ ​​một vectơ, bạn cần một vị từ như vậy.

À, khi hàm thực sự cần tự thao tác con trỏ thông minh.

Chính xác.

Trong trường hợp đó, tôi nghĩ chúng ta nên chuyển qua tham chiếu.

Đúng. Và nếu nó không thay đổi con trỏ, bạn muốn chuyển bằng tham chiếu const. Không cần sao chép vì bạn không cần chia sẻ quyền sở hữu. Đó là một kịch bản khác.

OK đã nhận nó. Hãy nói về kịch bản khác.

Người mà bạn chia sẻ quyền sở hữu? Đồng ý. Làm thế nào để bạn chia sẻ quyền sở hữu shared_ptr?

Bằng cách sao chép nó.

Sau đó, hàm sẽ cần tạo một bản sao của a shared_ptr, đúng không?

Chắc chắn. Vì vậy, tôi chuyển nó bằng một tham chiếu đến const và sao chép vào một biến cục bộ?

Không, đó là một sự bi quan. Nếu nó được truyền bằng tham chiếu, hàm sẽ không có lựa chọn nào khác ngoài việc tạo bản sao theo cách thủ công. Nếu nó được chuyển theo giá trị, trình biên dịch sẽ chọn lựa chọn tốt nhất giữa một bản sao và một lần di chuyển và thực hiện nó tự động. Vì vậy, hãy vượt qua giá trị.

Điểm tốt. Tôi phải nhớ rằng bài viết " Muốn Tốc độ? Vượt qua Giá trị. " Thường xuyên hơn.

Chờ đã, điều gì sẽ xảy ra nếu hàm lưu trữ shared_ptrtrong một biến thành viên, chẳng hạn? Điều đó sẽ không tạo ra một bản sao thừa phải không?

Hàm có thể chỉ cần di chuyển shared_ptrđối số vào bộ nhớ của nó. Di chuyển a shared_ptrlà rẻ vì nó không thay đổi bất kỳ số lượng tham chiếu nào.

À, ý kiến ​​hay.

Nhưng tôi đang nghĩ đến một kịch bản thứ ba: điều gì sẽ xảy ra nếu bạn không muốn thao túng shared_ptrhoặc chia sẻ quyền sở hữu?

Trong trường hợp đó, shared_ptrhoàn toàn không liên quan đến chức năng. Nếu bạn muốn thao túng con trỏ, hãy lấy con trỏ và để người gọi chọn ngữ nghĩa quyền sở hữu mà họ muốn.

Và tôi nên lấy pointee theo tham chiếu hay theo giá trị?

Các quy tắc thông thường được áp dụng. Con trỏ thông minh không thay đổi bất cứ điều gì.

Chuyển theo giá trị nếu tôi định sao chép, chuyển bằng tham chiếu nếu tôi muốn tránh sao chép.

Đúng.

Hừ! Tôi nghĩ bạn đã quên một kịch bản khác. Nếu tôi muốn chia sẻ quyền sở hữu, nhưng chỉ phụ thuộc vào một điều kiện nhất định thì sao?

Ah, một trường hợp cạnh thú vị. Tôi không mong điều đó xảy ra thường xuyên. Nhưng khi nó xảy ra, bạn có thể chuyển theo giá trị và bỏ qua bản sao nếu bạn không cần nó hoặc chuyển bằng tham chiếu và tạo bản sao nếu bạn cần.

Tôi mạo hiểm với một bản sao thừa trong tùy chọn đầu tiên và mất một nước đi tiềm năng trong tùy chọn thứ hai. Tôi không thể ăn bánh và có nó nữa?

Nếu bạn đang ở trong tình huống thực sự quan trọng, bạn có thể cung cấp hai quá tải, một lấy tham chiếu const lvalue và một lấy tham chiếu rvalue. Một bản sao, bản khác di chuyển. Một mẫu hàm chuyển tiếp hoàn hảo là một tùy chọn khác.

Tôi nghĩ rằng điều đó bao gồm tất cả các tình huống có thể xảy ra. Cảm ơn rât nhiều.


2
@Jon: Để làm gì? Đây là tất cả về tôi nên làm Một hay tôi nên làm B . Tôi nghĩ tất cả chúng ta đều biết cách chuyển các đối tượng theo giá trị / tham chiếu, phải không?
sbi

9
Bởi vì văn xuôi như "Điều đó có nghĩa là tôi sẽ chuyển nó bằng một tham chiếu đến const để tạo bản sao? Không, đó là một sự bi quan" gây nhầm lẫn cho người mới bắt đầu. Việc chuyển một tham chiếu đến const không tạo ra một bản sao.
Jon

1
@Martinho Tôi không đồng ý với quy tắc "Nếu bạn muốn thao túng con trỏ, hãy lấy một con trỏ". Như đã nêu trong câu trả lời này, việc không sở hữu tạm thời một đối tượng có thể nguy hiểm. Theo tôi, mặc định nên chuyển một bản sao cho quyền sở hữu tạm thời để đảm bảo tính hợp lệ của đối tượng trong suốt thời gian gọi hàm.
radman

@radman Không có trường hợp nào trong câu trả lời bạn liên kết áp dụng quy tắc đó, vì vậy nó hoàn toàn không liên quan. Vui lòng viết cho tôi một SSCCE nơi bạn chuyển qua tham chiếu từ một shared_ptr<T> ptr;(tức là void f(T&)được gọi với f(*ptr)) và đối tượng không tồn tại lâu hơn cuộc gọi. Ngoài ra, hãy viết một trong những nơi bạn vượt qua giá trị void f(T)và các cảnh cáo rắc rối.
R. Martinho Fernandes

@Martinho hãy xem mã ví dụ của tôi để biết ví dụ chính xác đơn giản. Ví dụ là cho void f(T&)và liên quan đến các chủ đề. Tôi nghĩ vấn đề của tôi là câu trả lời của bạn không khuyến khích chuyển quyền sở hữu shared_ptr's, đây là cách an toàn nhất, trực quan nhất và ít phức tạp nhất để sử dụng chúng. Tôi sẽ cực kỳ phản đối việc khuyên người mới bắt đầu trích xuất tham chiếu đến dữ liệu thuộc sở hữu của shared_ptr <> khả năng sử dụng sai là rất lớn và lợi ích là tối thiểu.
radman

22

Tôi nghĩ rằng mọi người sợ hãi một cách không cần thiết về việc sử dụng con trỏ thô làm tham số hàm. Nếu hàm sẽ không lưu trữ con trỏ hoặc ảnh hưởng đến thời gian tồn tại của nó, một con trỏ thô hoạt động tốt và đại diện cho mẫu số chung thấp nhất. Hãy xem xét ví dụ như cách bạn truyền a unique_ptrvào một hàm nhận a shared_ptrlàm tham số, theo giá trị hoặc bằng tham chiếu const?

void DoSomething(myClass * p);

DoSomething(myClass_shared_ptr.get());
DoSomething(myClass_unique_ptr.get());

Con trỏ thô làm tham số hàm không ngăn cản bạn sử dụng con trỏ thông minh trong mã gọi, nơi nó thực sự quan trọng.


8
Nếu bạn đang sử dụng một con trỏ, tại sao không chỉ sử dụng một tham chiếu? DoSomething(*a_smart_ptr)
Xeo

5
@Xeo, bạn nói đúng - điều đó sẽ còn tốt hơn. Tuy nhiên, đôi khi bạn cần cho phép khả năng xuất hiện con trỏ NULL.
Mark Ransom vào

1
Nếu bạn có quy ước sử dụng con trỏ thô làm tham số đầu ra, bạn sẽ dễ dàng phát hiện ra khi gọi mã mà một biến có thể thay đổi, ví dụ: so sánh fun(&x)với fun(x). Trong ví dụ sau xcó thể được truyền theo giá trị hoặc dưới dạng const ref. Nó sẽ như đã nêu trên cũng cho phép bạn vượt qua nullptrnếu bạn không quan tâm đến đầu ra ...
Andreas Magnusson

2
@AndreasMagnusson: Quy ước tham số con trỏ như-đầu ra có thể mang lại cảm giác an toàn sai khi xử lý các con trỏ được truyền vào từ một nguồn khác. Vì trong trường hợp đó, cuộc gọi vui (x) và * x được sửa đổi và nguồn không có & x để cảnh báo bạn.
Zan Lynx

điều gì sẽ xảy ra nếu lời gọi hàm tồn tại lâu hơn phạm vi của người gọi? Có một khả năng rằng destructor các ptr chia sẻ đã được gọi, và bây giờ là chức năng sẽ được cố gắng truy cập xóa bộ nhớ, kết quả là UB
Sridhar Thiagarajan

4

Vâng, toàn bộ ý tưởng về shared_ptr <> là nhiều phiên bản có thể chứa cùng một con trỏ thô và bộ nhớ bên dưới sẽ chỉ được giải phóng khi phiên bản cuối cùng của shared_ptr <> bị phá hủy.

Tôi sẽ tránh một con trỏ đến shared_ptr <> vì điều đó làm mất mục đích vì bạn đang xử lý lại raw_pointers.


2

Truyền giá trị trong ví dụ đầu tiên của bạn là an toàn nhưng có một thành ngữ hay hơn. Chuyển qua tham chiếu const khi có thể - tôi sẽ nói có ngay cả khi xử lý con trỏ thông minh. Ví dụ thứ hai của bạn không chính xác bị hỏng nhưng nó rất !???. Ngớ ngẩn, không hoàn thành bất cứ điều gì và đánh bại một phần quan điểm của những con trỏ thông minh, và sẽ khiến bạn rơi vào một thế giới đầy đau khổ khi bạn cố gắng tham khảo và sửa đổi mọi thứ.


3
Không ai ủng hộ việc đi ngang qua con trỏ, nếu bạn muốn nói là con trỏ thô. Việc chuyển qua tài liệu tham khảo nếu không có tranh chấp quyền sở hữu chắc chắn là thành ngữ. Đặc biệt, vấn đề shared_ptr<>là bạn phải tạo một bản sao để thực sự chia sẻ quyền sở hữu; nếu bạn có một tham chiếu hoặc con trỏ tới một shared_ptr<>thì bạn sẽ không chia sẻ bất cứ thứ gì và phải chịu các vấn đề về thời gian tồn tại giống như một tham chiếu hoặc con trỏ thông thường.
ildjarn

2
@ildjarn: Chuyển một tham chiếu không đổi là tốt trong trường hợp này. Bản gốc shared_ptrcủa lệnh gọi được đảm bảo tồn tại lâu hơn lệnh gọi hàm, vì vậy hàm được an toàn khi sử dụng shared_ptr<> const &. Nếu nó cần lưu trữ con trỏ cho một thời gian sau, nó sẽ cần phải sao chép (tại thời điểm này, một bản sao phải được tạo ra, thay vì giữ một tham chiếu), nhưng không cần phải chịu chi phí sao chép trừ khi bạn cần phải làm nó. Tôi sẽ chuyển một tham chiếu không đổi trong trường hợp này ....
David Rodríguez - dribeas

3
@David: " Truyền một tham chiếu không đổi là tốt trong trường hợp này. " Không có trong bất kỳ API lành mạnh nào - tại sao một API lại yêu cầu một loại con trỏ thông minh mà nó thậm chí không tận dụng thay vì chỉ lấy một tham chiếu const bình thường? Điều đó gần như tồi tệ khi thiếu hằng số đúng. Nếu quan điểm của bạn là về mặt kỹ thuật thì nó không gây tổn hại gì, thì tôi chắc chắn đồng ý, nhưng tôi không nghĩ đó là Điều đúng nên làm.
ildjarn

2
... mặt khác, nếu hàm không cần mở rộng thời gian tồn tại của đối tượng, thì bạn có thể xóa đối tượng shared_ptrkhỏi giao diện và chuyển một consttham chiếu trơn ( ) đến đối tượng trỏ.
David Rodríguez - dribeas

3
@David: Điều gì sẽ xảy ra nếu người tiêu dùng API của bạn không sử dụng ngay shared_ptr<>từ đầu? Bây giờ API của bạn thực sự chỉ là một cơn đau ở cổ, vì nó buộc người gọi phải thay đổi ngữ nghĩa suốt đời đối tượng của họ một cách vô nghĩa chỉ để sử dụng nó. Tôi không thấy điều gì đáng ủng hộ ở đó, ngoài việc nói rằng "nó không gây tổn hại gì về mặt kỹ thuật." Tôi không thấy có gì gây tranh cãi về việc 'nếu bạn không cần quyền sở hữu chung, thì đừng sử dụng shared_ptr<>'.
ildjarn

0

trong hàm của DoSomething bạn, bạn đang thay đổi một thành viên dữ liệu của một thể hiện của lớp, myClass vì vậy những gì bạn đang sửa đổi là đối tượng được quản lý (con trỏ thô) chứ không phải đối tượng (shared_ptr). Có nghĩa là tại điểm trả về của hàm này, tất cả các con trỏ dùng chung đến con trỏ thô được quản lý sẽ thấy thành viên dữ liệu của chúng: myClass::someFieldđã thay đổi thành một giá trị khác.

trong trường hợp này, bạn đang truyền một đối tượng cho một hàm với đảm bảo rằng bạn không sửa đổi nó (nói về shared_ptr không phải đối tượng sở hữu).

Thành ngữ để diễn đạt điều này là: a const ref, like so

void DoSomething(const std::shared_ptr<myClass>& arg)

Tương tự như vậy, bạn đang đảm bảo với người dùng chức năng của mình, rằng bạn không thêm chủ sở hữu khác vào danh sách chủ sở hữu của con trỏ thô. Tuy nhiên, bạn vẫn có khả năng sửa đổi đối tượng cơ bản được trỏ tới bởi con trỏ thô.

CAVEAT: Có nghĩa là, nếu bằng một cách nào đó ai đó gọi shared_ptr::resettrước khi bạn gọi hàm của bạn và tại thời điểm đó là shared_ptr cuối cùng sở hữu raw_ptr, thì đối tượng của bạn sẽ bị phá hủy và hàm của bạn sẽ thao tác một Con trỏ lơ lửng đến đối tượng bị phá hủy. RẤT NGUY HIỂM!!!

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.