Là tốt hơn trong C ++ để vượt qua giá trị hoặc vượt qua tham chiếu liên tục?


213

Là tốt hơn trong C ++ để vượt qua giá trị hoặc vượt qua tham chiếu liên tục?

Tôi đang tự hỏi đó là thực hành tốt hơn. Tôi nhận ra rằng việc chuyển qua tham chiếu liên tục sẽ cung cấp hiệu suất tốt hơn trong chương trình vì bạn không tạo bản sao của biến.


Câu trả lời:


203

Nó được sử dụng để thường được khuyến cáo thực hành tốt nhất 1 để sử dụng vượt qua bởi ref const cho tất cả các loại , trừ BUILTIN loại ( char, int, double, vv), cho vòng lặp và cho các đối tượng chức năng (lambdas, các lớp học bắt nguồn từ std::*_function).

Điều này đặc biệt đúng trước sự tồn tại của ngữ nghĩa di chuyển . Lý do rất đơn giản: nếu bạn chuyển qua giá trị, một bản sao của đối tượng phải được tạo và ngoại trừ các đối tượng rất nhỏ, điều này luôn đắt hơn so với việc chuyển tham chiếu.

Với C ++ 11, chúng tôi đã đạt được ngữ nghĩa di chuyển . Tóm lại, di chuyển ngữ nghĩa cho phép, trong một số trường hợp, một đối tượng có thể được truyền qua giá trị bởi giá trị mà không cần sao chép nó. Đặc biệt, đây là trường hợp khi đối tượng mà bạn đang đi qua là một rvalue .

Trong chính nó, di chuyển một đối tượng vẫn ít nhất là tốn kém như chuyển qua tham chiếu. Tuy nhiên, trong nhiều trường hợp, một hàm sẽ sao chép bên trong một đối tượng - nghĩa là nó sẽ sở hữu đối số. 2

Trong những tình huống này, chúng ta có sự đánh đổi (đơn giản hóa) sau:

  1. Chúng ta có thể vượt qua đối tượng bằng cách tham chiếu, sau đó sao chép nội bộ.
  2. Chúng ta có thể vượt qua đối tượng theo giá trị.

Truyền thông qua giá trị, vẫn còn khiến đối tượng bị sao chép, trừ khi đối tượng là một giá trị. Trong trường hợp của một giá trị, thay vào đó, đối tượng có thể được di chuyển, do đó, trường hợp thứ hai đột nhiên không còn là bản sao, sau đó di chuyển, nhưng di chuyển, sau đó (có khả năng) di chuyển lại một lần nữa.

Đối với các đối tượng lớn mà thực hiện nhà xây dựng di chuyển thích hợp (ví dụ như vectơ, dây ...), trường hợp thứ hai là sau đó bao la hiệu quả hơn là người đầu tiên. Do đó, nên sử dụng pass by value nếu hàm có quyền sở hữu đối số và nếu loại đối tượng hỗ trợ di chuyển hiệu quả .


Một ghi chú lịch sử:

Trong thực tế, bất kỳ trình biên dịch hiện đại nào cũng có thể tìm ra khi truyền theo giá trị là đắt và chuyển đổi cuộc gọi để sử dụng const ref nếu có thể.

Về lý thuyết. Trong thực tế, trình biên dịch không thể luôn thay đổi điều này mà không phá vỡ giao diện nhị phân của hàm. Trong một số trường hợp đặc biệt (khi chức năng được nội tuyến), bản sao sẽ thực sự bị xóa nếu trình biên dịch có thể tìm ra rằng đối tượng ban đầu sẽ không bị thay đổi thông qua các hành động trong chức năng.

Nhưng nói chung, trình biên dịch không thể xác định điều này và sự ra đời của ngữ nghĩa di chuyển trong C ++ đã khiến việc tối ưu hóa này ít liên quan hơn nhiều.


1 Ví dụ trong Scott Meyers, C ++ hiệu quả .

2 Điều này đặc biệt thường đúng đối với các nhà xây dựng đối tượng, có thể lấy các đối số và lưu trữ chúng bên trong để trở thành một phần của trạng thái của đối tượng được xây dựng.


hmmm ... tôi không chắc chắn rằng nó có giá trị để vượt qua ref. double-s
sergtk

3
Như thường lệ, boost giúp ở đây. boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm có công cụ mẫu để tự động tìm ra khi một loại là loại dựng sẵn (hữu ích cho các mẫu, đôi khi bạn không thể biết điều đó một cách dễ dàng).
CesarB

13
Câu trả lời này bỏ lỡ một điểm quan trọng. Để tránh cắt lát, bạn phải vượt qua bằng cách tham chiếu (const hoặc cách khác). Xem stackoverflow.com/questions/274626/
Mạnh

6
@Chris: đúng rồi. Tôi đã bỏ toàn bộ phần đa hình vì đó là một ngữ nghĩa hoàn toàn khác. Tôi tin rằng OP (về mặt ngữ nghĩa) có nghĩa là thông qua giá trị đối số thông qua. Khi các ngữ nghĩa khác được yêu cầu, câu hỏi thậm chí không đặt ra.
Konrad Rudolph

98

Chỉnh sửa: Bài viết mới của Dave Abrahams trên cpp-next:

Muốn tốc độ? Đi qua giá trị.


Truyền theo giá trị cho các cấu trúc trong đó việc sao chép rẻ tiền có lợi thế bổ sung mà trình biên dịch có thể cho rằng các đối tượng không bí danh (không phải là cùng một đối tượng). Sử dụng tham chiếu qua trình biên dịch có thể giả định rằng luôn luôn. Ví dụ đơn giản:

foo * f;

void bar(foo g) {
    g.i = 10;
    f->i = 2;
    g.i += 5;
}

trình biên dịch có thể tối ưu hóa nó thành

g.i = 15;
f->i = 2;

vì nó biết rằng f và g không chia sẻ cùng một vị trí. nếu g là một tham chiếu (foo &), trình biên dịch không thể giả định điều đó. vì gi sau đó có thể được đặt bí danh bởi f-> i và phải có giá trị là 7. vì vậy trình biên dịch sẽ phải tìm nạp lại giá trị mới của gi từ bộ nhớ.

Đối với các quy tắc cơ bản hơn, đây là một bộ quy tắc tốt được tìm thấy trong bài viết Move Con constructor (rất nên đọc).

  • Nếu hàm có ý định thay đổi đối số làm hiệu ứng phụ, hãy lấy nó bằng tham chiếu không phải là const.
  • Nếu hàm không sửa đổi đối số của nó và đối số là kiểu nguyên thủy, hãy lấy nó theo giá trị.
  • Mặt khác lấy nó bằng tham chiếu const, trừ các trường hợp sau
    • Nếu sau đó hàm cần phải tạo một bản sao của tham chiếu const, hãy lấy nó theo giá trị.

"Nguyên thủy" ở trên có nghĩa là về cơ bản các loại dữ liệu nhỏ dài vài byte và không đa hình (các trình lặp, các đối tượng hàm, v.v ...) hoặc tốn kém để sao chép. Trong bài báo đó, có một quy tắc khác. Ý tưởng là đôi khi người ta muốn tạo một bản sao (trong trường hợp không thể sửa đổi đối số) và đôi khi người ta không muốn (trong trường hợp người ta muốn sử dụng chính đối số trong hàm nếu đối số là tạm thời , ví dụ). Bài viết giải thích chi tiết làm thế nào có thể được thực hiện. Trong C ++ 1x, kỹ thuật đó có thể được sử dụng nguyên bản với sự hỗ trợ ngôn ngữ. Cho đến lúc đó, tôi sẽ đi với các quy tắc trên.

Ví dụ: Để tạo một chữ hoa chuỗi và trả về phiên bản chữ hoa, người ta phải luôn luôn vượt qua giá trị: Dù sao đi nữa, người ta phải lấy một bản sao của nó (không thể thay đổi trực tiếp tham chiếu const) người gọi và tạo bản sao đó sớm để người gọi có thể tối ưu hóa càng nhiều càng tốt - như chi tiết trong bài báo đó:

my::string uppercase(my::string s) { /* change s and return it */ }

Tuy nhiên, nếu bạn không cần thay đổi tham số, hãy lấy tham chiếu đến const:

bool all_uppercase(my::string const& s) { 
    /* check to see whether any character is uppercase */
}

Tuy nhiên, nếu mục đích của tham số là viết một cái gì đó vào đối số, thì hãy chuyển nó bằng tham chiếu không const

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}

tôi thấy quy tắc của bạn tốt nhưng tôi không chắc về phần đầu tiên mà bạn nói về việc không vượt qua nó như một giới thiệu sẽ tăng tốc nó. vâng chắc chắn, nhưng không vượt qua một cái gì đó như một ref chỉ là sự lạc quan hoàn toàn không có ý nghĩa gì cả. nếu bạn muốn thay đổi đối tượng ngăn xếp mà bạn đang truyền vào, hãy làm như vậy bằng ref. nếu bạn không, vượt qua nó bằng giá trị. nếu bạn không muốn thay đổi nó, hãy chuyển nó dưới dạng const-ref. tối ưu hóa đi kèm với giá trị truyền qua không quan trọng vì bạn có được những thứ khác khi chuyển qua làm ref. tôi không hiểu "muốn tốc độ?" Sice nếu bạn sẽ thực hiện những op bạn sẽ vượt qua bằng giá trị nào ..
chikuba

Johannes: Tôi thích bài viết đó khi tôi đọc nó, nhưng tôi đã thất vọng khi tôi thử nó. Mã này không thành công trên cả GCC và MSVC. Tôi đã bỏ lỡ một cái gì đó, hoặc nó không hoạt động trong thực tế?
dùng541686

Tôi không nghĩ rằng tôi đồng ý rằng nếu bạn muốn tạo một bản sao, bạn sẽ chuyển nó theo giá trị (thay vì const ref), sau đó di chuyển nó. Nhìn vào nó theo cách này, những gì hiệu quả hơn, một bản sao và di chuyển (bạn thậm chí có thể có 2 bản sao nếu bạn chuyển nó về phía trước), hoặc chỉ là một bản sao? Có một số trường hợp đặc biệt cho cả hai bên, nhưng nếu dữ liệu của bạn không thể được di chuyển bằng mọi cách (ví dụ: POD với hàng tấn số nguyên), không cần thêm bản sao.
Ion Todirel

2
Mehrdad, không chắc chắn những gì bạn mong đợi, nhưng mã hoạt động như mong đợi
Ion Todirel

Tôi xem xét sự cần thiết của việc sao chép chỉ để thuyết phục trình biên dịch rằng các loại không trùng lặp với sự thiếu hụt trong ngôn ngữ. Tôi thà sử dụng GCC __restrict__(cũng có thể hoạt động trên các tài liệu tham khảo) hơn là sao chép quá mức. C ++ tiêu chuẩn quá tệ đã không chấp nhận restricttừ khóa của C99 .
Ruslan

12

Phụ thuộc vào loại. Bạn đang thêm chi phí nhỏ để phải tham khảo và bổ nhiệm. Đối với các loại có kích thước bằng hoặc nhỏ hơn con trỏ đang sử dụng ctor sao chép mặc định, có thể sẽ nhanh hơn để vượt qua giá trị.


Đối với các loại không phải là bản địa, bạn có thể (tùy thuộc vào mức độ trình biên dịch tối ưu hóa mã), tăng hiệu suất bằng cách sử dụng tham chiếu const thay vì chỉ tham chiếu.
OJ.

9

Như đã được chỉ ra, nó phụ thuộc vào loại. Đối với các kiểu dữ liệu tích hợp, tốt nhất là truyền theo giá trị. Ngay cả một số cấu trúc rất nhỏ, chẳng hạn như một cặp số nguyên có thể hoạt động tốt hơn bằng cách chuyển qua giá trị.

Dưới đây là một ví dụ, giả sử bạn có một giá trị nguyên và bạn muốn chuyển nó sang một thói quen khác. Nếu giá trị đó đã được tối ưu hóa để được lưu trữ trong một thanh ghi, thì nếu bạn muốn chuyển nó thành tham chiếu, trước tiên nó phải được lưu trong bộ nhớ và sau đó một con trỏ tới bộ nhớ đó được đặt trên ngăn xếp để thực hiện cuộc gọi. Nếu nó được truyền theo giá trị, tất cả những gì được yêu cầu là thanh ghi được đẩy lên ngăn xếp. (Các chi tiết phức tạp hơn một chút so với các hệ thống gọi và CPU khác nhau).

Nếu bạn đang làm lập trình mẫu, bạn thường bị buộc phải luôn vượt qua const ref vì bạn không biết các loại được truyền vào. Việc vượt qua các hình phạt cho việc chuyển một cái gì đó xấu theo giá trị còn tệ hơn nhiều so với các hình phạt khi chuyển loại tích hợp bởi const ref.


Lưu ý về thuật ngữ: một cấu trúc chứa một triệu ints vẫn là "loại POD". Có thể bạn có nghĩa là 'đối với các loại tích hợp, tốt nhất là vượt qua giá trị'.
Steve Jessop

6

Đây là những gì tôi thường làm việc khi thiết kế giao diện của hàm không phải mẫu:

  1. Truyền theo giá trị nếu hàm không muốn sửa đổi tham số và giá trị rẻ để sao chép (int, double, float, char, bool, v.v ... Lưu ý rằng std :: string, std :: vector và phần còn lại của các container trong thư viện tiêu chuẩn là KHÔNG)

  2. Chuyển qua con trỏ const nếu giá trị đắt để sao chép và hàm không muốn sửa đổi giá trị được trỏ đến và NULL là giá trị mà hàm xử lý.

  3. Truyền bằng con trỏ không const nếu giá trị đắt để sao chép và hàm muốn sửa đổi giá trị được trỏ đến và NULL là giá trị mà hàm xử lý.

  4. Truyền tham chiếu const khi giá trị đắt để sao chép và hàm không muốn sửa đổi giá trị được đề cập và NULL sẽ không phải là giá trị hợp lệ nếu sử dụng con trỏ thay thế.

  5. Chuyển qua tham chiếu không phải là const khi giá trị đắt để sao chép và hàm muốn sửa đổi giá trị được đề cập và NULL sẽ không phải là giá trị hợp lệ nếu sử dụng con trỏ thay thế.


Thêm std::optionalvào hình ảnh và bạn không còn cần con trỏ.
Hươu cao cổ Violet

5

Âm thanh như bạn đã có câu trả lời của bạn. Vượt qua giá trị là tốn kém, nhưng cung cấp cho bạn một bản sao để làm việc nếu bạn cần nó.


Tôi không chắc tại sao điều này đã được bỏ phiếu? Nó có ý nghĩa với tôi. Nếu bạn sẽ cần giá trị hiện được lưu trữ, sau đó chuyển qua giá trị. Nếu không, vượt qua các tài liệu tham khảo.
Totty

4
Nó là hoàn toàn phụ thuộc loại. Thực hiện kiểu POD (dữ liệu cũ đơn giản) theo tham chiếu trên thực tế có thể làm giảm hiệu suất bằng cách gây ra nhiều truy cập bộ nhớ hơn.
Torlack

1
Rõ ràng vượt qua int bằng cách tham chiếu không tiết kiệm bất cứ điều gì! Tôi nghĩ rằng câu hỏi ngụ ý những thứ lớn hơn một con trỏ.
GeekyMonkey

4
Điều đó không rõ ràng, tôi đã thấy rất nhiều mã bởi những người không thực sự hiểu cách máy tính làm việc vượt qua những điều đơn giản bởi const ref vì họ đã nói rằng đó là điều tốt nhất để làm.
Torlack

4

Như một quy tắc đi qua tham chiếu const là tốt hơn. Nhưng nếu bạn cần sửa đổi đối số chức năng cục bộ, bạn nên sử dụng chuyển qua giá trị. Đối với một số loại cơ bản, hiệu suất nói chung giống nhau cho cả việc truyền theo giá trị và theo tham chiếu. Trên thực tế, tham chiếu được biểu thị bằng con trỏ, đó là lý do tại sao bạn có thể mong đợi rằng ví dụ cho cả hai con trỏ đều giống nhau về hiệu suất, hoặc thậm chí chuyển qua giá trị có thể nhanh hơn vì không cần thiết.


Nếu bạn cần sửa đổi bản sao tham số của callee, bạn có thể tạo một bản sao trong mã được gọi thay vì chuyển theo giá trị. IMO bạn thường không nên chọn API dựa trên một chi tiết triển khai như thế: nguồn của mã cuộc gọi cũng giống như vậy, nhưng mã đối tượng của nó thì không.
Steve Jessop

Nếu bạn vượt qua bản sao giá trị được tạo ra. Và IMO không có vấn đề gì trong cách bạn tạo một bản sao: thông qua đối số chuyển qua giá trị hoặc cục bộ - đây là điều liên quan đến C ++. Nhưng từ quan điểm thiết kế tôi đồng ý với bạn. Nhưng tôi chỉ mô tả các tính năng C ++ ở đây và không chạm vào thiết kế.
sergtk

1

Như một quy tắc chung, giá trị cho các loại không phải là lớp và tham chiếu const cho các lớp. Nếu một lớp thực sự nhỏ, có lẽ tốt hơn để vượt qua giá trị, nhưng sự khác biệt là tối thiểu. Điều bạn thực sự muốn tránh là vượt qua một số lớp khổng lồ theo giá trị và có tất cả trùng lặp - điều này sẽ tạo ra sự khác biệt lớn nếu bạn vượt qua, giả sử, một vectơ std :: có khá nhiều yếu tố trong đó.


Sự hiểu biết của tôi là std::vectorthực sự phân bổ các mục của nó trên đống và đối tượng vector không bao giờ phát triển. Oh, đợi đã. Tuy nhiên, nếu hoạt động gây ra một bản sao của vectơ được thực hiện, trên thực tế, nó sẽ đi và sao chép tất cả các phần tử. Điều đó thật tệ.
Steven Lu

1
Vâng, đó là những gì tôi đã nghĩ. sizeof(std::vector<int>)là hằng số, nhưng chuyển nó theo giá trị vẫn sẽ sao chép nội dung trong trường hợp không có bất kỳ sự thông minh nào của trình biên dịch.
Peter

1

Truyền theo giá trị cho các loại nhỏ.

Chuyển qua tham chiếu const cho các loại lớn (định nghĩa lớn có thể khác nhau giữa các máy) NHƯNG, trong C ++ 11, truyền theo giá trị nếu bạn sẽ sử dụng dữ liệu, vì bạn có thể khai thác ngữ nghĩa di chuyển. Ví dụ:

class Person {
 public:
  Person(std::string name) : name_(std::move(name)) {}
 private:
  std::string name_;
};

Bây giờ mã gọi sẽ làm:

Person p(std::string("Albert"));

Và chỉ có một đối tượng sẽ được tạo và di chuyển trực tiếp vào thành viên name_trong lớp Person. Nếu bạn vượt qua tham chiếu const, một bản sao sẽ phải được tạo để đưa nó vào name_.


-5

Khác biệt đơn giản: - Trong chức năng chúng ta có tham số đầu vào và đầu ra, vì vậy nếu tham số đầu vào và đầu ra của bạn giống nhau thì hãy sử dụng lệnh gọi theo tham chiếu khác nếu tham số đầu vào và đầu ra khác nhau thì tốt hơn nên sử dụng gọi theo giá trị.

thí dụ void amount(int account , int deposit , int total )

tham số đầu vào: tài khoản, thông số đầu ra tiền gửi: tổng

đầu vào và đầu ra là cách sử dụng khác nhau của vaule

  1. void amount(int total , int deposit )

tổng đầu vào tổng tiền gửi đầu ra

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.