Là pass-by-value có phải là mặc định hợp lý trong C ++ 11 không?


142

Trong C ++ truyền thống, việc truyền giá trị vào các hàm và phương thức là chậm đối với các đối tượng lớn và thường được tán thành. Thay vào đó, các lập trình viên C ++ có xu hướng chuyển các tham chiếu xung quanh, nhanh hơn, nhưng đưa ra tất cả các loại câu hỏi phức tạp xung quanh quyền sở hữu và đặc biệt là về quản lý bộ nhớ (trong trường hợp đối tượng được phân bổ heap)

Bây giờ, trong C ++ 11, chúng ta có các tham chiếu Rvalue và di chuyển các hàm tạo, điều đó có nghĩa là có thể thực hiện một đối tượng lớn (như một std::vector) giá rẻ để truyền giá trị vào và ra khỏi hàm.

Vì vậy, điều này có nghĩa là mặc định sẽ được truyền theo giá trị cho các thể hiện của các loại như std::vectorstd::string? Còn đối với các đối tượng tùy chỉnh thì sao? Thực hành mới tốt nhất là gì?


22
pass by reference ... which introduces all sorts of complicated questions around ownership and especially around memory management (in the event that the object is heap-allocated). Tôi không hiểu làm thế nào nó phức tạp hoặc có vấn đề cho quyền sở hữu? Tôi có thể bỏ lỡ điều gì không?
iammilind

1
@iammilind: Một ví dụ từ kinh nghiệm cá nhân. Một chủ đề có một đối tượng chuỗi. Nó được truyền cho một hàm sinh ra một luồng khác, nhưng người gọi không biết hàm này đã lấy chuỗi đó const std::string&và không phải là một bản sao. Chuỗi đầu tiên sau đó đã thoát ...
Zan Lynx

12
@ZanLynx: Nghe có vẻ như là một chức năng rõ ràng không bao giờ được thiết kế để được gọi là chức năng luồng.
Nicol Bolas

5
Đồng ý với iammilind, tôi không thấy có vấn đề gì. Chuyển qua tham chiếu const phải là mặc định của bạn cho các đối tượng "lớn" và theo giá trị cho các đối tượng nhỏ hơn. Tôi đặt giới hạn giữa lớn và nhỏ ở khoảng 16 byte (hoặc 4 con trỏ trên hệ thống 32 bit).
JN

3
Herb Sutter trở lại vấn đề cơ bản! Những điều cốt yếu của bài thuyết trình C ++ hiện đại tại CppCon đã đi sâu vào khá nhiều chi tiết về điều này. Video tại đây .
Chris đã vẽ

Câu trả lời:


138

Đó là một mặc định hợp lý nếu bạn cần tạo một bản sao bên trong cơ thể. Đây là những gì Dave Abrahams đang ủng hộ :

Hướng dẫn: Không sao chép đối số chức năng của bạn. Thay vào đó, chuyển chúng theo giá trị và để trình biên dịch thực hiện sao chép.

Trong mã này có nghĩa là không làm điều này:

void foo(T const& t)
{
    auto copy = t;
    // ...
}

nhưng làm điều này:

void foo(T t)
{
    // ...
}

có lợi thế mà người gọi có thể sử dụng foonhư vậy:

T lval;
foo(lval); // copy from lvalue
foo(T {}); // (potential) move from prvalue
foo(std::move(lval)); // (potential) move from xvalue

và chỉ có công việc tối thiểu được thực hiện. Bạn sẽ cần hai quá tải để làm tương tự với các tham chiếu void foo(T const&);void foo(T&&);.

Với ý nghĩ đó, bây giờ tôi đã viết các hàm tạo có giá trị của mình như sau:

class T {
    U u;
    V v;
public:
    T(U u, V v)
        : u(std::move(u))
        , v(std::move(v))
    {}
};

Nếu không, chuyển qua tham chiếu đến constvẫn là hợp lý.


29
+1, đặc biệt là cho bit cuối cùng :) Không nên quên rằng Move Con constructor chỉ có thể được gọi nếu đối tượng để di chuyển không được dự kiến ​​sẽ không thay đổi sau đó: SomeProperty p; for (auto x: vec) { x.foo(p); }không phù hợp, chẳng hạn. Ngoài ra, Move Con constructor có chi phí (đối tượng càng lớn thì chi phí càng cao) trong khi const&về cơ bản là miễn phí.
Matthieu M.

25
@MatthieuM. Nhưng điều quan trọng là phải biết "đối tượng càng lớn thì chi phí càng cao" của việc di chuyển thực sự có nghĩa là gì: "lớn hơn" thực sự có nghĩa là "càng có nhiều biến thành viên". Chẳng hạn, việc di chuyển std::vectormột triệu phần tử có chi phí giống như di chuyển một phần tử với năm phần tử do chỉ con trỏ tới mảng trên heap được di chuyển, không phải mọi đối tượng trong vectơ. Vì vậy, nó không thực sự là một vấn đề lớn.
Lucas

+1 Tôi cũng có xu hướng sử dụng cấu trúc pass-by-value-then-move kể từ khi tôi bắt đầu sử dụng C ++ 11. Điều này làm cho tôi cảm thấy hơi khó chịu, vì mã của tôi hiện có ở std::movekhắp mọi nơi ..
stijn

1
Có một rủi ro với const&, điều đó đã khiến tôi tăng gấp ba lần. void foo(const T&); int main() { S s; foo(s); }. Điều này có thể biên dịch, mặc dù các loại khác nhau, nếu có một hàm tạo T lấy tham số S làm đối số. Điều này có thể chậm, bởi vì một đối tượng T lớn có thể đang được xây dựng. Bạn có thể nghĩ rằng bạn đã chuyển một tài liệu tham khảo mà không cần sao chép, nhưng có thể bạn là như vậy. Xem câu trả lời này cho một câu hỏi tôi yêu cầu thêm. Về cơ bản, &thường chỉ liên kết với giá trị, nhưng có một ngoại lệ cho rvalue. Có những lựa chọn thay thế.
Aaron McDaid

1
@AaronMcDaid Đây là tin cũ, theo nghĩa đó là điều bạn luôn phải biết ngay cả trước C ++ 11. Và không có gì thay đổi nhiều về điều đó.
Luc Danton

71

Trong hầu hết các trường hợp, ngữ nghĩa của bạn phải là:

bar(foo f); // want to obtain a copy of f
bar(const foo& f); // want to read f
bar(foo& f); // want to modify f

Tất cả các chữ ký khác chỉ nên được sử dụng một cách tiết kiệm và với sự biện minh tốt. Trình biên dịch bây giờ sẽ khá nhiều luôn luôn làm việc này theo cách hiệu quả nhất. Bạn chỉ có thể nhận được bằng cách viết mã của bạn!


2
Mặc dù tôi thích vượt qua một con trỏ nếu tôi sẽ sửa đổi một đối số. Tôi đồng ý với hướng dẫn về phong cách của Google rằng điều này làm cho rõ ràng hơn rằng đối số sẽ được sửa đổi mà không cần kiểm tra kỹ chữ ký của hàm ( google-styleguide.googlecode.com/svn/trunk/ tựa ).
Max Lybbert

40
Lý do mà tôi không thích chuyển con trỏ là vì nó thêm trạng thái thất bại có thể vào các chức năng của tôi. Tôi cố gắng viết tất cả các chức năng của mình để chúng có thể chính xác, vì nó giúp giảm đáng kể không gian cho các lỗi ẩn nấp. foo(bar& x) { x.a = 3; }Đây là một cách đáng tin cậy hơn nhiều (và có thể đọc được!) foo(bar* x) {if (!x) throw std::invalid_argument("x"); x->a = 3;
So

22
@Max Lybbert: Với tham số con trỏ, bạn không cần kiểm tra chữ ký của hàm, nhưng bạn cần kiểm tra tài liệu để biết liệu bạn có được phép truyền con trỏ null hay không, nếu hàm sẽ có quyền sở hữu, v.v. IMHO, một tham số con trỏ truyền tải thông tin ít hơn nhiều so với tham chiếu không const. Tuy nhiên, tôi đồng ý rằng sẽ rất tốt nếu có manh mối trực quan tại trang web cuộc gọi rằng đối số có thể được sửa đổi (như reftừ khóa trong C #).
Luc Touraille

Liên quan đến việc chuyển theo giá trị và dựa vào ngữ nghĩa di chuyển, tôi cảm thấy ba lựa chọn này làm tốt hơn việc giải thích mục đích sử dụng của tham số. Đây là những hướng dẫn tôi luôn luôn làm theo.
Trevor Hickey

1
@AaronMcDaid is shared_ptr intended to never be null? Much as (I think) unique_ptr is?Cả hai giả định đó đều không chính xác. unique_ptrshared_ptrcó thể giữ null / nullptrgiá trị. Nếu bạn không muốn lo lắng về các giá trị null, bạn nên sử dụng các tham chiếu, bởi vì chúng không bao giờ có thể là null. Bạn cũng sẽ không phải gõ ->, điều mà bạn thấy khó chịu :)
Julian

10

Truyền tham số theo giá trị nếu bên trong thân hàm bạn cần một bản sao của đối tượng hoặc chỉ cần di chuyển đối tượng. Đi qua const&nếu bạn chỉ cần truy cập không đột biến vào đối tượng.

Ví dụ sao chép đối tượng:

void copy_antipattern(T const& t) { // (Don't do this.)
    auto copy = t;
    t.some_mutating_function();
}

void copy_pattern(T t) { // (Do this instead.)
    t.some_mutating_function();
}

Ví dụ di chuyển đối tượng:

std::vector<T> v; 

void move_antipattern(T const& t) {
    v.push_back(t); 
}

void move_pattern(T t) {
    v.push_back(std::move(t)); 
}

Ví dụ truy cập không đột biến:

void read_pattern(T const& t) {
    t.some_const_function();
}

Để biết lý do, hãy xem những bài đăng trên blog của Dave AbrahamsXiang Fan .


0

Chữ ký của một chức năng sẽ phản ánh mục đích sử dụng của nó. Khả năng đọc là quan trọng, cũng cho trình tối ưu hóa.

Đây là điều kiện tiên quyết tốt nhất để một trình tối ưu hóa tạo mã nhanh nhất - về lý thuyết ít nhất và nếu không có trong thực tế thì trong một vài năm thực tế.

Xem xét hiệu suất rất thường được đánh giá cao trong bối cảnh truyền tham số. Chuyển tiếp hoàn hảo là một ví dụ. Các chức năng như emplace_backhầu hết là rất ngắn và nội tuyến.

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.