Chúng ta nên vượt qua shared_ptr theo tham chiếu hoặc theo giá trị?


270

Khi một hàm mất shared_ptr(từ boost hoặc C ++ 11 STL), bạn có vượt qua nó không:

  • bởi const tham khảo: void foo(const shared_ptr<T>& p)

  • hoặc theo giá trị : void foo(shared_ptr<T> p)?

Tôi thích phương pháp đầu tiên vì tôi nghi ngờ nó sẽ nhanh hơn. Nhưng điều này có thực sự đáng giá hay có bất kỳ vấn đề nào khác không?

Bạn có thể vui lòng đưa ra lý do cho sự lựa chọn của bạn hoặc nếu trường hợp, tại sao bạn nghĩ rằng nó không quan trọng.


14
Vấn đề là những cái đó không tương đương. Phiên bản tham chiếu hét lên "Tôi sẽ bí danh một số shared_ptrvà tôi có thể thay đổi nó nếu tôi muốn.", Trong khi phiên bản giá trị nói "Tôi sẽ sao chép của bạn shared_ptr, vì vậy trong khi tôi có thể thay đổi thì bạn sẽ không bao giờ biết. ) Tham số tham chiếu const là giải pháp thực sự, có nội dung "Tôi sẽ đặt bí danh cho một số người shared_ptrvà tôi hứa sẽ không thay đổi nó." (Điều này cực kỳ giống với ngữ nghĩa giá trị!)
GManNickG

2
Này, tôi sẽ quan tâm đến ý kiến ​​của các bạn về việc trả lại một shared_ptrthành viên trong lớp. Bạn có làm điều đó bằng const-refs không?
Julian Schaub - litb

Khả năng thứ ba là sử dụng std :: move () với C ++ 0x, điều này hoán đổi cả hai shared_ptr
Tomaka17

@Johannes: Tôi sẽ trả lại nó bằng const-Reference chỉ để tránh mọi sự sao chép / đếm lại. Sau đó, một lần nữa, tôi trả lại tất cả các thành viên bằng cách tham chiếu const trừ khi họ là người nguyên thủy.
GManNickG

Câu trả lời:


229

Câu hỏi này đã được Scott, Andrei và Herb thảo luận và trả lời trong phiên Ask Us Anything tại C ++ và Beyond 2011 . Xem từ 4:34 về shared_ptrhiệu suất và tính chính xác .

Một thời gian ngắn, không có lý do nào để vượt qua giá trị, trừ khi mục tiêu là chia sẻ quyền sở hữu đối tượng (ví dụ: giữa các cấu trúc dữ liệu khác nhau hoặc giữa các luồng khác nhau).

Trừ khi bạn có thể di chuyển - tối ưu hóa nó như được giải thích bởi Scott Meyers trong video thảo luận được liên kết ở trên, nhưng điều đó có liên quan đến phiên bản thực tế của C ++ mà bạn có thể sử dụng.

Một bản cập nhật lớn cho cuộc thảo luận này đã xảy ra trong Bảng tương tác của hội nghị GoNative 2012 : Hỏi chúng tôi bất cứ điều gì! rất đáng xem, đặc biệt là từ 22h50 .


5
nhưng như được hiển thị ở đây, giá rẻ hơn để vượt qua theo giá trị: stackoverflow.com/a/12002668/128384 cũng không nên được tính đến (ít nhất là đối với các đối số của nhà xây dựng, v.v ... trong đó một shared_ptr sẽ trở thành thành viên của lớp)?
Stijn

2
@stijn Có và không. Câu hỏi và trả lời của bạn không đầy đủ, trừ khi nó làm rõ phiên bản của tiêu chuẩn C ++ mà nó đề cập đến. Nó rất dễ dàng để truyền bá chung không bao giờ / luôn luôn các quy tắc đơn giản là gây hiểu lầm. Trừ khi, độc giả dành thời gian để làm quen với bài viết và tài liệu tham khảo của David Abrahams, hoặc xem ngày đăng so với tiêu chuẩn C ++ hiện tại. Vì vậy, cả câu trả lời, của tôi và câu trả lời của bạn, đều đúng trong thời gian đăng.
mloskot

1
" trừ khi có đa luồng " không, MT không có cách nào đặc biệt.
tò mò

3
Tôi đến bữa tiệc rất muộn, nhưng lý do của tôi muốn vượt qua shared_ptr theo giá trị là nó làm cho mã ngắn hơn và đẹp hơn. Nghiêm túc. Value*ngắn và dễ đọc, nhưng nó rất tệ, vì vậy bây giờ mã của tôi đã đầy const shared_ptr<Value>&và nó ít đọc hơn đáng kể và chỉ ... ít gọn gàng hơn. Những gì trước đây void Function(Value* v1, Value* v2, Value* v3)là bây giờ void Function(const shared_ptr<Value>& v1, const shared_ptr<Value>& v2, const shared_ptr<Value>& v3), và mọi người vẫn ổn với điều này?
Alex

7
@Alex Thực tiễn phổ biến là tạo bí danh (typedefs) ngay sau lớp học. Ví dụ của bạn: class Value {...}; using ValuePtr = std::shared_ptr<Value>;Sau đó, chức năng của bạn trở nên đơn giản hơn: void Function(const ValuePtr& v1, const ValuePtr& v2, const ValuePtr& v3)và bạn có được hiệu suất tối đa. Đó là lý do tại sao bạn sử dụng C ++, phải không? :)
4LegsDrivenCat

92

Đây là Herb Sutter của

Hướng dẫn: Không chuyển con trỏ thông minh làm tham số chức năng trừ khi bạn muốn sử dụng hoặc tự thao tác con trỏ thông minh, chẳng hạn như để chia sẻ hoặc chuyển quyền sở hữu.

Hướng dẫn: Thể hiện rằng một hàm sẽ lưu trữ và chia sẻ quyền sở hữu đối tượng heap bằng cách sử dụng tham số shared_ptr theo giá trị.

Hướng dẫn: Chỉ sử dụng một tham số không phải là const_ptr & để sửa đổi shared_ptr. Chỉ sử dụng const shared_ptr & làm tham số nếu bạn không chắc chắn liệu mình có lấy bản sao và chia sẻ quyền sở hữu hay không; mặt khác sử dụng widget * thay vào đó (hoặc nếu không thể null, một widget &).


3
Cảm ơn các liên kết đến Sutter. Đó là một bài viết tuyệt vời. Tôi không đồng ý với anh ấy trên widget *, thích tùy chọn <widget &> nếu C ++ 14 khả dụng. widget * quá mơ hồ từ mã cũ.
Eponymous

3
+1 để bao gồm widget * và widget & như khả năng. Chỉ cần giải thích, chuyển widget * hoặc widget & có lẽ là tùy chọn tốt nhất khi hàm không kiểm tra / sửa đổi chính đối tượng con trỏ. Giao diện tổng quát hơn, vì nó không yêu cầu một loại con trỏ cụ thể và vấn đề hiệu năng của số tham chiếu shared_ptr được tránh.
tgnottingham

4
Tôi nghĩ rằng đây nên là câu trả lời được chấp nhận ngày hôm nay, bởi vì hướng dẫn thứ hai. Nó rõ ràng vô hiệu hóa câu trả lời được chấp nhận hiện tại, có nghĩa là: không có lý do để vượt qua giá trị.
mbrt

62

Cá nhân tôi sẽ sử dụng một consttài liệu tham khảo. Không cần phải tăng số tham chiếu chỉ để giảm nó một lần nữa vì lợi ích của một cuộc gọi chức năng.


1
Tôi đã không bỏ phiếu cho câu trả lời của bạn, nhưng trước đây đây là vấn đề ưu tiên, có những ưu và nhược điểm đối với mỗi trong hai khả năng để xem xét. Và sẽ rất tốt nếu biết và thảo luận về những ưu và nhược điểm. Sau đó mọi người có thể đưa ra quyết định cho mình.
Danvil

@Danvil: cân nhắc về cách thức shared_ptrhoạt động, nhược điểm duy nhất có thể xảy ra khi không vượt qua tham chiếu là một sự mất mát nhỏ trong hiệu suất. Có hai nguyên nhân ở đây. a) tính năng răng cưa con trỏ có nghĩa là con trỏ có giá trị dữ liệu cộng với bộ đếm (có thể là 2 cho các ref yếu) được sao chép, do đó sẽ tốn kém hơn một chút khi sao chép vòng dữ liệu. b) đếm tham chiếu nguyên tử chậm hơn một chút so với mã tăng / giảm cũ đơn giản, nhưng là cần thiết để đảm bảo an toàn cho luồng. Ngoài ra, hai phương pháp là giống nhau cho hầu hết các ý định và mục đích.
Evan Teran

37

Đi qua consttham khảo, nó nhanh hơn. Nếu bạn cần lưu trữ nó, hãy nói trong một số container, ref. số lượng sẽ được tự động tăng lên một cách kỳ diệu bởi hoạt động sao chép.


4
Downvote cho ý kiến ​​của mình mà không có bất kỳ số nào để sao lưu nó.
kwesolowski

22

Tôi chạy vào mã bên dưới, một lần với foolấy shared_ptrbằng const&và một lần nữa với foolấy shared_ptrtheo giá trị.

void foo(const std::shared_ptr<int>& p)
{
    static int x = 0;
    *p = ++x;
}

int main()
{
    auto p = std::make_shared<int>();
    auto start = clock();
    for (int i = 0; i < 10000000; ++i)
    {
        foo(p);
    }    
    std::cout << "Took " << clock() - start << " ms" << std::endl;
}

Sử dụng VS2015, bản phát hành x86, trên bộ xử lý intel core 2 quad (2.4GHz) của tôi

const shared_ptr&     - 10ms  
shared_ptr            - 281ms 

Bản sao theo phiên bản giá trị là một thứ tự cường độ chậm hơn.
Nếu bạn đang gọi một chức năng đồng bộ từ luồng hiện tại, hãy chọn const&phiên bản.


1
Bạn có thể nói trình biên dịch, nền tảng và tối ưu hóa nào bạn đã sử dụng không?
Carlton

Tôi đã sử dụng bản dựng gỡ lỗi của vs2015, cập nhật câu trả lời để sử dụng bản dựng phát hành ngay bây giờ.
tcb

1
Tôi tò mò nếu bật tối ưu hóa, bạn sẽ nhận được kết quả tương tự với cả hai
Elliot Woods

2
Tối ưu hóa không giúp được gì nhiều. vấn đề là sự tranh chấp về số lượng tham chiếu trên bản sao.
Alex

1
Đó không phải là vấn đề. Một foo()hàm như vậy thậm chí không nên chấp nhận một con trỏ dùng chung ở vị trí đầu tiên vì nó không sử dụng đối tượng này: nó nên chấp nhận int&và thực hiện p = ++x;, gọi foo(*p);từ main(). Một hàm chấp nhận một đối tượng con trỏ thông minh khi nó cần làm gì đó với nó và hầu hết thời gian, điều bạn cần làm là di chuyển nó ( std::move()) sang một nơi khác, vì vậy một tham số theo giá trị không có chi phí.
eepp

15

Vì C ++ 11, bạn nên lấy nó theo giá trị so với const & thường xuyên hơn bạn nghĩ.

Nếu bạn đang dùng std :: shared_ptr (chứ không phải loại T cơ bản), thì bạn đang làm như vậy vì bạn muốn làm gì đó với nó.

Nếu bạn muốn sao chép nó ở đâu đó, sẽ có ý nghĩa hơn khi lấy nó bằng cách sao chép và std :: di chuyển nó bên trong, thay vì lấy nó bằng const & và sau đó sao chép nó. Điều này là do bạn cho phép người gọi tùy chọn lần lượt std :: di chuyển shared_ptr khi gọi hàm của bạn, do đó tiết kiệm cho mình một tập hợp các hoạt động tăng và giảm. Hay không. Nghĩa là, người gọi hàm có thể quyết định liệu anh ta có cần std :: shared_ptr hay không sau khi gọi hàm, và tùy thuộc vào việc có di chuyển hay không. Điều này là không thể đạt được nếu bạn vượt qua const &, và do đó tốt nhất là lấy nó theo giá trị.

Tất nhiên, nếu cả hai người gọi đều cần shared_ptr của mình lâu hơn (do đó không thể std :: di chuyển nó) và bạn không muốn tạo một bản sao đơn giản trong hàm (giả sử bạn muốn một con trỏ yếu hoặc đôi khi bạn chỉ muốn để sao chép nó, tùy thuộc vào một số điều kiện), sau đó một const & có thể vẫn thích hợp hơn.

Ví dụ, bạn nên làm

void enqueue(std::shared<T> t) m_internal_queue.enqueue(std::move(t));

kết thúc

void enqueue(std::shared<T> const& t) m_internal_queue.enqueue(t);

Bởi vì trong trường hợp này bạn luôn tạo một bản sao trong nội bộ


1

Không biết chi phí thời gian của hoạt động sao chép shared_copy trong đó tăng và giảm nguyên tử, tôi gặp phải vấn đề sử dụng CPU cao hơn nhiều. Tôi không bao giờ mong đợi sự gia tăng và giảm nguyên tử có thể tốn nhiều chi phí như vậy.

Theo kết quả thử nghiệm của tôi, tăng và giảm nguyên tử int32 mất 2 hoặc 40 lần so với tăng và giảm không nguyên tử. Tôi đã nhận nó trên 3GHz Core i7 với Windows 8.1. Kết quả trước xuất hiện khi không có tranh chấp xảy ra, kết quả sau khi khả năng tranh chấp cao xảy ra. Tôi nhớ rằng các hoạt động nguyên tử là ở khóa dựa trên phần cứng cuối cùng. Khóa là khóa. Xấu đến hiệu suất khi xảy ra tranh chấp.

Trải qua điều này, tôi luôn sử dụng byref (const shared_ptr &) hơn byval (shared_ptr).


1

Có một bài đăng trên blog gần đây: https://medium.com/@vgasparyan1995/pass-by-value-vs-pass-by-reference-to-const-c-f8944171e3ce

Vì vậy, câu trả lời cho điều này là: Làm (gần như) không bao giờ đi qua const shared_ptr<T>&.
Đơn giản chỉ cần vượt qua lớp cơ bản thay thế.

Về cơ bản các loại tham số hợp lý duy nhất là:

  • shared_ptr<T> - Sửa đổi và sở hữu
  • shared_ptr<const T> - Đừng sửa đổi, hãy sở hữu
  • T& - Sửa đổi, không có quyền sở hữu
  • const T& - Không sửa đổi, không có quyền sở hữu
  • T - Không sửa đổi, không có quyền sở hữu, Giá rẻ để sao chép

Như @accel đã chỉ ra trong https://stackoverflow.com/a/26197326/1930508 lời khuyên từ Herb Sutter là:

Chỉ sử dụng const shared_ptr & làm tham số nếu bạn không chắc chắn liệu mình có lấy bản sao và chia sẻ quyền sở hữu hay không

Nhưng trong bao nhiêu trường hợp bạn không chắc chắn? Vì vậy, đây là một tình huống hiếm gặp


0

Vấn đề đã biết là việc chia sẻ shared_ptr theo giá trị có chi phí và nên tránh nếu có thể.

Chi phí chuyển qua shared_ptr

Hầu hết thời gian vượt qua shared_ptr bằng tham chiếu, và thậm chí tốt hơn bởi tham chiếu const, sẽ làm.

Nguyên tắc cốt lõi cpp có một quy tắc cụ thể để truyền shared_ptr

R.34: Lấy tham số shared_ptr để thể hiện rằng hàm là chủ sở hữu một phần

void share(shared_ptr<widget>);            // share -- "will" retain refcount

Một ví dụ về việc khi chia sẻ shared_ptr theo giá trị là thực sự cần thiết là khi người gọi chuyển một đối tượng được chia sẻ sang một callee không đồng bộ - tức là người gọi đi ra khỏi phạm vi trước khi callee hoàn thành công việc của mình. Callee phải "kéo dài" thời gian tồn tại của đối tượng được chia sẻ bằng cách lấy share_ptr theo giá trị. Trong trường hợp này, chuyển tham chiếu đến shared_ptr sẽ không làm.

Cũng vậy với việc chuyển một đối tượng chia sẻ đến một luồng công việc.


-4

shared_ptr không đủ lớn, cũng như hàm tạo \ hàm hủy của nó làm đủ công việc để có đủ chi phí từ bản sao để quan tâm đến việc chuyển bằng tham chiếu so với hiệu suất sao chép.


15
Bạn đã đo nó chưa?
tò mò

2
@stonemetal: Điều gì về hướng dẫn nguyên tử trong khi tạo shared_ptr mới?
Quarra

Đây là loại không phải POD, do đó, trong hầu hết các ABI thậm chí chuyển nó "theo giá trị" thực sự vượt qua một con trỏ. Đây không phải là bản sao thực sự của byte mà là vấn đề. Như bạn có thể thấy trong đầu ra asm, việc truyền shared_ptr<int>giá trị theo hơn 100 lệnh x86 (bao gồm các lockhướng dẫn ed đắt tiền để tăng / giảm số lượng ref) về mặt nguyên tử). Truyền bằng ref liên tục giống như chuyển một con trỏ tới bất cứ thứ gì (và trong ví dụ này trên trình thám hiểm trình biên dịch Godbolt, tối ưu hóa cuộc gọi đuôi biến điều này thành một jmp đơn giản thay vì một cuộc gọi: godbolt.org/g/TazMBU ).
Peter Cordes

TL: DR: Đây là C ++, nơi các nhà xây dựng sao chép có thể thực hiện nhiều công việc hơn là chỉ sao chép các byte. Câu trả lời này là tổng số rác.
Peter Cordes

2
stackoverflow.com/questions/3628081/shared-ptr-horrible-speed Như một ví dụ chung con trỏ thông qua giá trị vs vượt qua bằng cách tham khảo ông thấy một sự khác biệt thời gian chạy xấp xỉ 33%. Nếu bạn đang làm việc với mã quan trọng về hiệu năng thì con trỏ trần sẽ giúp bạn tăng hiệu suất lớn hơn. Vì vậy, chắc chắn vượt qua const ref nếu bạn nhớ nhưng nó không phải là một vấn đề lớn nếu bạn không. Điều quan trọng hơn nhiều là không sử dụng shared_ptr nếu bạn không cần nó.
né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.