Làm thế nào để vượt qua std :: unique_ptr xung quanh?


83

Tôi đang có lần thử đầu tiên sử dụng C ++ 11 unique_ptr; Tôi đang thay thế một con trỏ thô đa hình bên trong một dự án của tôi, được sở hữu bởi một lớp, nhưng được chuyển qua khá thường xuyên.

Tôi đã từng có các chức năng như:

bool func(BaseClass* ptr, int other_arg) {
  bool val;
  // plain ordinary function that does something...
  return val;
}

Nhưng tôi sớm nhận ra rằng tôi sẽ không thể chuyển sang:

bool func(std::unique_ptr<BaseClass> ptr, int other_arg);

Bởi vì người gọi sẽ phải xử lý quyền sở hữu con trỏ đối với hàm, điều tôi không muốn. Vì vậy, giải pháp tốt nhất cho vấn đề của tôi là gì?

Mặc dù tôi chuyển con trỏ dưới dạng tham chiếu, như thế này:

bool func(const std::unique_ptr<BaseClass>& ptr, int other_arg);

Nhưng tôi cảm thấy rất không thoải mái khi làm như vậy, thứ nhất vì có vẻ như không theo bản năng khi chuyển một thứ gì đó đã được nhập _ptrlàm tham chiếu, điều gì sẽ là tham chiếu của tham chiếu. Thứ hai vì chữ ký hàm càng lớn hơn. Thứ ba, vì trong mã được tạo, sẽ cần hai lần chuyển hướng con trỏ liên tiếp để đến được biến của tôi.

Câu trả lời:


97

Nếu bạn muốn hàm sử dụng con trỏ, hãy chuyển một tham chiếu đến nó. Không có lý do gì để buộc chức năng chỉ hoạt động với một số loại con trỏ thông minh:

bool func(BaseClass& base, int other_arg);

Và tại trang web cuộc gọi sử dụng operator*:

func(*some_unique_ptr, 42);

Ngoài ra, nếu baseđối số được phép để trống, hãy giữ nguyên chữ ký và sử dụng get()hàm thành viên:

bool func(BaseClass* base, int other_arg);
func(some_unique_ptr.get(), 42);

4
Điều này là tốt, nhưng bạn có thể loại bỏ cam std::unique_ptrđể std::vector<std::unique_ptr>tranh cãi?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

31

Lợi thế của việc sử dụng std::unique_ptr<T> (ngoài việc không phải nhớ gọi deletehoặc gọi delete[]rõ ràng) là nó đảm bảo rằng một con trỏ nullptrhoặc nó trỏ đến một thể hiện hợp lệ của đối tượng (cơ sở). Tôi sẽ quay lại vấn đề này sau khi tôi trả lời câu hỏi của bạn, nhưng thông báo đầu tiên là NÊN sử dụng con trỏ thông minh để quản lý thời gian tồn tại của các đối tượng được phân bổ động.

Bây giờ, của bạn vấn đề thực sự là làm thế nào để sử dụng điều này với mã cũ của bạn .

Đề xuất của tôi là nếu bạn không muốn chuyển nhượng hoặc chia sẻ quyền sở hữu, bạn nên luôn chuyển các tham chiếu đến đối tượng. Khai báo hàm của bạn như thế này (có hoặc không constcó định tính, nếu cần):

bool func(BaseClass& ref, int other_arg) { ... }

Sau đó, người gọi, có std::shared_ptr<BaseClass> ptr di chúc sẽ xử lý nullptrtrường hợp hoặc nó sẽ yêu cầu bool func(...)tính toán kết quả:

if (ptr) {
  result = func(*ptr, some_int);
} else {
  /* the object was, for some reason, either not created or destroyed */
}

Điều này có nghĩa là bất kỳ người gọi nào cũng phải hứa rằng tham chiếu là hợp lệ và nó sẽ tiếp tục hợp lệ trong suốt quá trình thực thi thân hàm.


Đây là lý do tại sao tôi thực sự tin rằng bạn không nên chuyển con trỏ thô hoặc tham chiếu đến con trỏ thông minh.

Một con trỏ thô chỉ là một địa chỉ bộ nhớ. Có thể có một trong (ít nhất) 4 nghĩa:

  1. Địa chỉ của một khối bộ nhớ nơi đặt đối tượng mong muốn của bạn. ( tốt )
  2. Địa chỉ 0x0 mà bạn có thể chắc chắn là không thể tham khảo và có thể có ngữ nghĩa là "không có gì" hoặc "không có đối tượng". ( xấu )
  3. Địa chỉ của một khối bộ nhớ nằm ngoài không gian địa chỉ của quy trình của bạn (tham khảo nó hy vọng sẽ khiến chương trình của bạn gặp sự cố). ( xấu xí )
  4. Địa chỉ của một khối bộ nhớ có thể được tham chiếu đến nhưng không chứa những gì bạn mong đợi. Có thể con trỏ đã vô tình bị sửa đổi và bây giờ nó trỏ đến một địa chỉ có thể ghi khác (của một biến hoàn toàn khác trong quy trình của bạn). Việc ghi vào vị trí bộ nhớ này đôi khi sẽ gây ra rất nhiều điều thú vị trong quá trình thực thi, bởi vì hệ điều hành sẽ không phàn nàn miễn là bạn được phép ghi ở đó. ( Zoinks! )

Việc sử dụng con trỏ thông minh một cách chính xác sẽ làm giảm bớt các trường hợp khá đáng sợ 3 và 4, thường không thể phát hiện được tại thời điểm biên dịch và bạn thường chỉ gặp phải trong thời gian chạy khi chương trình của bạn gặp sự cố hoặc xảy ra những điều không mong muốn.

Chuyển con trỏ thông minh làm đối số có hai nhược điểm: bạn không thể thay đổi const-ness của đối tượng được trỏ mà không tạo bản sao (điều này thêm chi phí cho shared_ptrvà không thể cho unique_ptr), và bạn vẫn còn lại với nghĩa thứ hai ( nullptr).

Tôi đánh dấu trường hợp thứ hai là ( xấu ) từ góc độ thiết kế. Đây là một lập luận tế nhị hơn về trách nhiệm.

Hãy tưởng tượng ý nghĩa của nó khi một hàm nhận được nullptr làm tham số của nó. Đầu tiên nó phải quyết định xem phải làm gì với nó: sử dụng một giá trị "ma thuật" thay cho đối tượng bị mất tích? thay đổi hành vi hoàn toàn và tính toán một cái gì đó khác (mà không yêu cầu đối tượng)? hoảng sợ và ném một ngoại lệ? Hơn nữa, điều gì sẽ xảy ra khi hàm nhận 2, 3 hoặc thậm chí nhiều đối số hơn bởi con trỏ thô? Nó phải kiểm tra từng người trong số họ và điều chỉnh hành vi của nó cho phù hợp. Điều này bổ sung một cấp độ hoàn toàn mới trên đầu xác thực đầu vào mà không có lý do thực sự.

Người gọi phải là người có đủ thông tin về ngữ cảnh để đưa ra những quyết định này, hay nói cách khác, điều tồi tệ sẽ ít đáng sợ hơn khi bạn biết nhiều hơn. Mặt khác, hàm chỉ nên thực hiện lời hứa của người gọi rằng bộ nhớ mà nó được trỏ tới là an toàn để hoạt động như dự kiến. (Tài liệu tham khảo vẫn là địa chỉ bộ nhớ, nhưng về mặt khái niệm đại diện cho một lời hứa hợp lệ.)


25

Tôi đồng ý với Martinho, nhưng tôi nghĩ điều quan trọng là phải chỉ ra ngữ nghĩa quyền sở hữu của một tham chiếu chuyển qua. Tôi nghĩ giải pháp chính xác là sử dụng một tham chiếu chuyển qua đơn giản ở đây:

bool func(BaseClass& base, int other_arg);

Ý nghĩa thường được chấp nhận của tham chiếu truyền trong C ++ giống như thể người gọi hàm nói với hàm "ở đây, bạn có thể mượn đối tượng này, sử dụng nó và sửa đổi nó (nếu không phải là const), nhưng chỉ đối với thời hạn của cơ quan chức năng. " Điều này không hề mâu thuẫn với các quy tắc sở hữu củaunique_ptr đối tượng vì đối tượng chỉ là được cho mượn trong thời gian ngắn, không có sự chuyển giao quyền sở hữu thực sự (nếu cho ai đó mượn xe thì bạn có ký giấy chủ quyền không qua anh ta?).

Vì vậy, mặc dù việc kéo tham chiếu (hoặc thậm chí là con trỏ thô) ra khỏi tham chiếu (hoặc thậm chí là con trỏ thô) có vẻ không tốt ( unique_ptrvì nó hoàn toàn phù hợp với các quy tắc sở hữu do cái unique_ptr. Và sau đó, tất nhiên, có những lợi thế tuyệt vời khác, như cú pháp rõ ràng, không hạn chế đối với chỉ các đối tượng thuộc sở hữu của a unique_ptr, v.v.


0

Cá nhân tôi tránh lấy một tham chiếu từ một con trỏ / con trỏ thông minh. Bởi vì điều gì sẽ xảy ra nếu con trỏ là nullptr? Nếu bạn thay đổi chữ ký thành này:

bool func(BaseClass& base, int other_arg);

Bạn có thể phải bảo vệ mã của mình khỏi các tham chiếu con trỏ rỗng:

if (the_unique_ptr)
  func(*the_unique_ptr, 10);

Nếu lớp là chủ sở hữu duy nhất của con trỏ, thì phương án thứ hai của Martinho có vẻ hợp lý hơn:

func(the_unique_ptr.get(), 10);

Ngoài ra, bạn có thể sử dụng std::shared_ptr. Tuy nhiên, nếu có một tổ chức duy nhất chịu trách nhiệm delete, std::shared_ptrchi phí sẽ không trả hết.


Bạn có biết rằng, trong hầu hết các nhiệm vụ, std::unique_ptrkhông có chi phí chung, phải không?
lvella

1
Vâng, tôi biết điều đó. Đó là lý do tại sao tôi nói với anh ấy về std::shared_ptrchi phí.
Guilherme Ferreira
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.