Làm cách nào để trả về con trỏ thông minh (shared_ptr), theo tham chiếu hoặc theo giá trị?


94

Giả sử tôi có một lớp với phương thức trả về a shared_ptr.

Những lợi ích và hạn chế có thể có của việc trả lại bằng cách tham khảo hoặc theo giá trị là gì?

Hai manh mối có thể có:

  • Sự phá hủy đối tượng sớm. Nếu tôi trả về shared_ptrtham chiếu by (const), thì bộ đếm tham chiếu không được tăng lên, vì vậy tôi phải chịu rủi ro bị xóa đối tượng khi nó vượt ra khỏi phạm vi trong ngữ cảnh khác (ví dụ: một luồng khác). Điều này có chính xác? Điều gì sẽ xảy ra nếu môi trường là một luồng, tình huống này cũng có thể xảy ra?
  • Giá cả. Giá trị vượt qua chắc chắn không miễn phí. Nó có đáng để tránh nó bất cứ khi nào có thể?

Cảm ơn tất cả mọi người.

Câu trả lời:


114

Trả lại con trỏ thông minh theo giá trị.

Như bạn đã nói, nếu bạn trả lại nó bằng cách tham chiếu, bạn sẽ không tăng số lượng tham chiếu đúng cách, điều này dẫn đến nguy cơ xóa nội dung nào đó vào thời điểm không thích hợp. Chỉ điều đó thôi cũng đủ lý do để không quay lại bằng cách tham khảo. Các giao diện phải mạnh mẽ.

Mối quan tâm về chi phí ngày nay được tranh cãi nhờ tối ưu hóa giá trị trả về (RVO), vì vậy bạn sẽ không phải chịu một trình tự tăng-tăng-giảm hoặc tương tự như vậy trong các trình biên dịch hiện đại. Vì vậy, cách tốt nhất để trả về a shared_ptrlà chỉ cần trả về theo giá trị:

shared_ptr<T> Foo()
{
    return shared_ptr<T>(/* acquire something */);
};

Đây là một cơ hội RVO rõ ràng cho các trình biên dịch C ++ hiện đại. Tôi biết thực tế là các trình biên dịch Visual C ++ triển khai RVO ngay cả khi tất cả các tối ưu hóa bị tắt. Và với ngữ nghĩa di chuyển của C ++ 11, mối quan tâm này thậm chí còn ít liên quan hơn. (Nhưng cách duy nhất để chắc chắn là lập hồ sơ và thử nghiệm.)

Nếu bạn vẫn chưa thuyết phục, Dave Abrahams có một bài báo đưa ra lập luận cho việc trả về theo giá trị. Tôi sao chép một đoạn mã ở đây; Tôi thực sự khuyên bạn nên đọc toàn bộ bài viết:

Hãy trung thực: đoạn mã sau đây khiến bạn cảm thấy thế nào?

std::vector<std::string> get_names();
...
std::vector<std::string> const names = get_names();

Thành thật mà nói, mặc dù tôi nên biết rõ hơn, nó khiến tôi lo lắng. Về nguyên tắc, khi get_names() trả về, chúng ta phải sao chép một vectortrong số strings. Sau đó, chúng ta cần sao chép nó một lần nữa khi khởi tạo namesvà chúng ta cần hủy bản sao đầu tiên. Nếu có N strings trong vectơ, mỗi bản sao có thể yêu cầu tối đa N + 1 phân bổ bộ nhớ và một loạt các truy cập dữ liệu không thân thiện với bộ nhớ cache> khi nội dung chuỗi được sao chép.

Thay vì đối mặt với loại lo lắng đó, tôi thường quay trở lại tham khảo qua để tránh các bản sao không cần thiết:

get_names(std::vector<std::string>& out_param );
...
std::vector<std::string> names;
get_names( names );

Thật không may, cách tiếp cận này là xa lý tưởng.

  • Mã tăng 150%
  • Chúng tôi đã phải bỏ const-ness vì chúng tôi đang biến đổi tên.
  • Như các lập trình viên chức năng muốn nhắc nhở chúng ta, đột biến làm cho mã phức tạp hơn để suy luận bằng cách làm suy yếu tính minh bạch của tham chiếu và suy luận cân bằng.
  • Chúng tôi không còn có ngữ nghĩa giá trị nghiêm ngặt cho tên.

Nhưng có thực sự cần thiết phải xáo trộn mã của chúng ta theo cách này để đạt được hiệu quả không? May mắn thay, câu trả lời hóa ra là không (và đặc biệt là không nếu bạn đang sử dụng C ++ 0x).


Tôi không biết rằng tôi sẽ nói RVO làm cho câu hỏi tranh luận vì việc trả lại bằng cách tham chiếu quyết định khiến RVO không thể thực hiện được.
Edward Strange

@CrazyEddie: Đúng, đó là một trong những lý do tại sao tôi khuyên bạn nên trả về OP theo giá trị.
Trong silico

Quy tắc RVO, được tiêu chuẩn cho phép, có vượt trội hơn các quy tắc về mối quan hệ đồng bộ hóa / xảy ra trước đó, được đảm bảo bởi tiêu chuẩn không?
edA-qa mort-hay-y

1
@ edA-qa mort-ora-y: RVO được cho phép một cách rõ ràng ngay cả khi nó có những tác dụng phụ. Ví dụ: nếu bạn có một cout << "Hello World!";câu lệnh trong hàm tạo mặc định và sao chép, bạn sẽ không thấy hai Hello World!s khi RVO có hiệu lực. Tuy nhiên, đây không phải là vấn đề đối với các con trỏ thông minh được thiết kế đúng cách, ngay cả khi đồng bộ hóa wrt.
Trong silico

23

Về bất kỳ con trỏ thông minh nào (không chỉ shared_ptr), tôi không nghĩ rằng việc trả về một tham chiếu đến một tham chiếu được chấp nhận và tôi sẽ rất do dự khi chuyển chúng bằng tham chiếu hoặc con trỏ thô. Tại sao? Bởi vì bạn không thể chắc chắn rằng nó sẽ không bị sao chép nông cạn thông qua tham chiếu sau này. Điểm đầu tiên của bạn xác định lý do tại sao điều này nên được quan tâm. Điều này có thể xảy ra ngay cả trong môi trường đơn luồng. Bạn không cần truy cập đồng thời vào dữ liệu để đưa ngữ nghĩa sao chép xấu vào chương trình của mình. Bạn không thực sự kiểm soát những gì người dùng của mình làm với con trỏ sau khi bạn chuyển nó đi, vì vậy, không khuyến khích lạm dụng cung cấp cho người dùng API của bạn đủ dây để treo cổ họ.

Thứ hai, hãy xem việc triển khai con trỏ thông minh của bạn, nếu có thể. Việc xây dựng và phá hủy nên gần như không đáng kể. Nếu chi phí này không được chấp nhận, thì đừng sử dụng con trỏ thông minh! Nhưng ngoài điều này, bạn cũng sẽ cần phải kiểm tra kiến ​​trúc đồng thời mà bạn có, bởi vì quyền truy cập loại trừ lẫn nhau vào cơ chế theo dõi việc sử dụng con trỏ sẽ làm chậm bạn hơn là chỉ xây dựng đối tượng shared_ptr.

Chỉnh sửa, 3 năm sau: với sự ra đời của các tính năng hiện đại hơn trong C ++, tôi sẽ điều chỉnh câu trả lời của mình để dễ chấp nhận hơn các trường hợp khi bạn chỉ viết một lambda không bao giờ nằm ​​ngoài phạm vi của hàm gọi, và không sao chép ở một nơi khác. Ở đây, nếu bạn muốn tiết kiệm chi phí rất nhỏ của việc sao chép một con trỏ được chia sẻ, nó sẽ công bằng và an toàn. Tại sao? Bởi vì bạn có thể đảm bảo rằng tài liệu tham khảo sẽ không bao giờ bị sử dụng sai.

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.