Là những ngày trôi qua const std :: string & như một tham số trên?


604

Tôi nghe một cuộc nói chuyện gần đây của Herb Sutter người gợi ý rằng những lý do để vượt qua std::vectorstd::stringbởi const &phần lớn biến mất. Ông đề nghị rằng viết một chức năng như sau đây là thích hợp hơn:

std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}

Tôi hiểu rằng giá trị return_valsẽ là một giá trị tại điểm mà hàm trả về và do đó có thể được trả về bằng cách sử dụng ngữ nghĩa di chuyển, rất rẻ. Tuy nhiên, invalvẫn lớn hơn nhiều so với kích thước của một tham chiếu (thường được thực hiện như một con trỏ). Điều này là do a std::stringcó các thành phần khác nhau bao gồm một con trỏ vào heap và một thành viên char[]để tối ưu hóa chuỗi ngắn. Vì vậy, dường như với tôi rằng đi qua tham khảo vẫn là một ý tưởng tốt.

Bất cứ ai có thể giải thích tại sao Herb có thể đã nói điều này?


89
Tôi nghĩ rằng câu trả lời tốt nhất cho câu hỏi có lẽ là đọc bài viết của Dave Abrahams về nó trên C ++ Next . Tôi nói thêm rằng tôi không thấy gì về điều này đủ điều kiện là lạc đề hoặc không mang tính xây dựng. Đó là một câu hỏi rõ ràng, về lập trình, trong đó có câu trả lời thực tế.
Jerry Coffin

Hấp dẫn, vì vậy nếu bạn sẽ phải tạo một bản sao, thì giá trị truyền qua có thể nhanh hơn so với tham chiếu qua.
Benj

3
@Sz. Tôi nhạy cảm với các câu hỏi được phân loại sai thành trùng lặp và đóng. Tôi không nhớ chi tiết về trường hợp này và chưa xem xét lại chúng. Thay vào đó tôi chỉ đơn giản là sẽ xóa bình luận của tôi về giả định rằng tôi đã phạm sai lầm. Cảm ơn bạn đã mang đến sự chú ý của tôi.
Howard Hinnant

2
@HowardHinnant, cảm ơn bạn rất nhiều, đó luôn là khoảnh khắc quý giá khi người ta bắt gặp mức độ chăm chú và nhạy cảm này, thật là mới mẻ! (Tất nhiên tôi sẽ xóa của tôi sau đó.)
Sz.

Câu trả lời:


393

Lý do Herb nói những gì anh nói là vì những trường hợp như thế này.

Giả sử tôi có chức năng Agọi chức năng B, chức năng nào gọi chức năng C. Và Avượt qua một chuỗi thông qua Bvà vào C. Akhông biết hoặc không quan tâm đến C; tất cả Ađều biết về là B. Đó là, Clà một chi tiết thực hiện B.

Hãy nói rằng A được định nghĩa như sau:

void A()
{
  B("value");
}

Nếu B và C lấy chuỗi theo const&, thì nó trông giống như thế này:

void B(const std::string &str)
{
  C(str);
}

void C(const std::string &str)
{
  //Do something with `str`. Does not store it.
}

Tất cả đều tốt và tốt. Bạn chỉ đi qua con trỏ xung quanh, không sao chép, không di chuyển, mọi người đều vui vẻ. Cmất một const&vì nó không lưu trữ chuỗi. Nó chỉ đơn giản là sử dụng nó.

Bây giờ, tôi muốn thực hiện một thay đổi đơn giản: Ccần lưu trữ chuỗi ở đâu đó.

void C(const std::string &str)
{
  //Do something with `str`.
  m_str = str;
}

Xin chào, sao chép hàm tạo và cấp phát bộ nhớ tiềm năng (bỏ qua Tối ưu hóa chuỗi ngắn (SSO) ). Ngữ nghĩa di chuyển của C ++ 11 được cho là có thể loại bỏ việc xây dựng bản sao không cần thiết, phải không? Và Avượt qua tạm thời; không có lý do tại sao Cphải sao chép dữ liệu. Nó chỉ nên bỏ qua với những gì đã được trao cho nó.

Ngoại trừ nó không thể. Vì phải mất a const&.

Nếu tôi thay đổi Cđể lấy tham số của nó theo giá trị, điều đó chỉ gây ra Bviệc sao chép vào tham số đó; Tôi không đạt được gì.

Vì vậy, nếu tôi vừa chuyển qua strgiá trị thông qua tất cả các hàm, dựa vào std::moveviệc xáo trộn dữ liệu xung quanh, chúng ta sẽ không gặp phải vấn đề này. Nếu ai đó muốn giữ nó, họ có thể. Nếu họ không, oh tốt.

Nó có đắt hơn không? Đúng; di chuyển vào một giá trị đắt hơn so với sử dụng tài liệu tham khảo. Nó có rẻ hơn bản sao không? Không cho các chuỗi nhỏ với SSO. Có đáng để làm không?

Nó phụ thuộc vào trường hợp sử dụng của bạn. Bao nhiêu bạn ghét phân bổ bộ nhớ?


2
Khi bạn nói rằng việc chuyển sang một giá trị đắt hơn so với sử dụng tài liệu tham khảo, điều đó vẫn đắt hơn bởi một lượng không đổi (không phụ thuộc vào độ dài của chuỗi được di chuyển) phải không?
Neil G

3
@NeilG: Bạn có hiểu "phụ thuộc vào việc thực hiện" nghĩa là gì không? Những gì bạn đang nói là sai, bởi vì nó phụ thuộc vào việc và cách thức triển khai SSO.
ildjarn

17
@ildjarn: Trong phân tích thứ tự, nếu trường hợp xấu nhất của một thứ bị ràng buộc bởi một hằng số, thì đó vẫn là thời gian không đổi. Không có một chuỗi nhỏ dài nhất? Chuỗi đó có mất một lượng thời gian liên tục để sao chép không? Không phải tất cả các chuỗi nhỏ hơn mất ít thời gian hơn để sao chép? Sau đó, sao chép chuỗi cho các chuỗi nhỏ là "thời gian không đổi" trong phân tích thứ tự - mặc dù các chuỗi nhỏ mất nhiều thời gian khác nhau để sao chép. Phân tích thứ tự có liên quan với hành vi tiệm cận .
Neil G

8
@NeilG: Chắc chắn, nhưng câu hỏi ban đầu của bạn là " vẫn còn đắt hơn bởi một lượng không đổi (không phụ thuộc vào độ dài của chuỗi được di chuyển) phải không? " Điểm tôi đang cố gắng là, nó có thể đắt hơn bởi sự khác biệt số lượng không đổi tùy thuộc vào độ dài của chuỗi, được tính tổng là "không".
ildjarn

13
Tại sao chuỗi sẽ movedtừ B đến C trong trường hợp giá trị? Nếu B là B(std::string b)và C C(std::string c)thì chúng ta phải gọi C(std::move(b))B hoặc bphải không thay đổi (do đó 'không di chuyển từ') cho đến khi thoát B. (Có lẽ một trình biên dịch tối ưu hóa sẽ chuyển chuỗi dưới như-nếu quy tắc nếu bkhông được sử dụng sau khi cuộc gọi nhưng tôi không nghĩ rằng có một bảo đảm vững mạnh.) Điều này cũng đúng đối với các bản sao của strđể m_str. Ngay cả khi một tham số hàm được khởi tạo với một giá trị thì đó là một giá trị bên trong hàm và std::moveđược yêu cầu di chuyển từ giá trị đó.
Nhà hóa học 16/07/2015

163

Là những ngày trôi qua const std :: string & như một tham số trên?

Không . Nhiều người thực hiện lời khuyên này (bao gồm Dave Abrahams) ngoài phạm vi mà nó áp dụng và đơn giản hóa nó để áp dụng cho tất cả std::string tham số - Luôn luôn vượt qua std::stringgiá trị không phải là "cách thực hành tốt nhất" cho bất kỳ và tất cả các tham số và ứng dụng tùy ý vì tối ưu hóa các tham số này các bài nói / bài viết tập trung vào việc chỉ áp dụng cho một tập hợp các trường hợp bị hạn chế .

Nếu bạn đang trả về một giá trị, làm thay đổi tham số hoặc lấy giá trị, thì việc truyền theo giá trị có thể tiết kiệm được việc sao chép đắt tiền và mang lại sự thuận tiện về cú pháp.

Đã bao giờ, chuyển qua tham chiếu const tiết kiệm nhiều bản sao khi bạn không cần một bản sao .

Bây giờ đến ví dụ cụ thể:

Tuy nhiên inval vẫn lớn hơn khá nhiều so với kích thước của một tham chiếu (thường được thực hiện như một con trỏ). Điều này là do chuỗi std :: có các thành phần khác nhau bao gồm một con trỏ vào heap và char thành viên [] để tối ưu hóa chuỗi ngắn. Vì vậy, dường như với tôi rằng đi qua tham khảo vẫn là một ý tưởng tốt. Bất cứ ai có thể giải thích tại sao Herb có thể đã nói điều này?

Nếu kích thước ngăn xếp là một mối quan tâm (và giả sử điều này không được nội tuyến / tối ưu hóa), return_val+ inval> return_val- IOW, việc sử dụng ngăn xếp cực đại có thể được giảm bằng cách chuyển theo giá trị ở đây (lưu ý: quá đơn giản hóa ABI). Trong khi đó, chuyển qua tham chiếu const có thể vô hiệu hóa tối ưu hóa. Lý do chính ở đây không phải là để tránh tăng trưởng ngăn xếp, nhưng để đảm bảo tối ưu hóa có thể được thực hiện ở nơi áp dụng .

Những ngày trôi qua bởi tham chiếu const không kết thúc - các quy tắc phức tạp hơn trước đây. Nếu hiệu suất là quan trọng, bạn sẽ khôn ngoan khi xem xét cách bạn vượt qua các loại này, dựa trên các chi tiết bạn sử dụng trong triển khai của mình.


3
Về việc sử dụng ngăn xếp, ABI điển hình sẽ vượt qua một tham chiếu trong một thanh ghi mà không sử dụng ngăn xếp.
ahcox

63

Điều này phụ thuộc nhiều vào việc thực hiện của trình biên dịch.

Tuy nhiên, nó cũng phụ thuộc vào những gì bạn sử dụng.

Hãy xem xét các chức năng tiếp theo:

bool foo1( const std::string v )
{
  return v.empty();
}
bool foo2( const std::string & v )
{
  return v.empty();
}

Các chức năng này được thực hiện trong một đơn vị biên dịch riêng biệt để tránh nội tuyến. Sau đó:
1. Nếu bạn chuyển một nghĩa đen cho hai chức năng này, bạn sẽ không thấy nhiều sự khác biệt trong màn trình diễn. Trong cả hai trường hợp, một đối tượng chuỗi phải được tạo
2. Nếu bạn vượt qua một đối tượng chuỗi std :: khác, foo2sẽ tốt hơn foo1, vì foo1sẽ thực hiện một bản sao sâu.

Trên PC của tôi, sử dụng g ++ 4.6.1, tôi đã nhận được các kết quả sau:

  • biến theo tham chiếu: 1000000000 lần lặp -> thời gian trôi qua: 2.25912 giây
  • biến theo giá trị: 1000000000 lần lặp -> thời gian trôi qua: 27.2259 giây
  • bằng chữ theo tham chiếu: 100000000 lần lặp -> thời gian trôi qua: 9.10319 giây
  • bằng chữ theo giá trị: 100000000 lần lặp -> thời gian trôi qua: 8.62659 giây

4
Điều gì có liên quan hơn là những gì đang xảy ra bên trong hàm: nếu nó được gọi với một tham chiếu, có cần tạo một bản sao bên trong có thể được bỏ qua khi chuyển qua giá trị không?
leftaroundabout

1
@leftaroundabout Vâng, tất nhiên rồi. Giả định của tôi rằng cả hai chức năng đang làm chính xác cùng một điều.
BЈовић

5
Đó không phải là quan điểm của tôi. Việc chuyển theo giá trị hay bằng tham chiếu tốt hơn tùy thuộc vào những gì bạn đang thực hiện bên trong hàm. Trong ví dụ của bạn, bạn không thực sự sử dụng nhiều đối tượng chuỗi, vì vậy tham chiếu rõ ràng là tốt hơn. Nhưng nếu nhiệm vụ của hàm là đặt chuỗi trong một số cấu trúc hoặc để thực hiện, giả sử, một số thuật toán đệ quy liên quan đến nhiều phần tách của chuỗi, chuyển qua giá trị thực sự có thể lưu một số sao chép, so với chuyển qua tham chiếu. Nicol Bolas giải thích nó khá tốt.
leftaroundabout

3
Đối với tôi "nó phụ thuộc vào những gì bạn làm bên trong hàm" là thiết kế tồi - vì bạn đang căn cứ chữ ký của hàm trên các phần bên trong của việc thực hiện.
Hans Olsson

1
Có thể là một lỗi đánh máy, nhưng hai thời gian theo nghĩa đen cuối cùng có số vòng lặp ít hơn 10 lần.
TankorSmash

54

Câu trả lời ngắn gọn: KHÔNG! Câu trả lời dài:

  • Nếu bạn sẽ không sửa đổi chuỗi (coi là chỉ đọc), hãy chuyển chuỗi đó thành const ref&.
    ( const ref&rõ ràng cần phải ở trong phạm vi trong khi chức năng sử dụng nó thực thi)
  • Nếu bạn có kế hoạch sửa đổi nó hoặc bạn biết nó sẽ thoát khỏi phạm vi (luồng) , hãy chuyển nó dưới dạng value, đừng sao chép const ref&bên trong thân hàm của bạn.

Có một bài đăng trên cpp-next.com có tên "Muốn tốc độ, vượt qua giá trị!" . TL; DR:

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.

DỊCH DỊCH ^

Đừng sao chép các đối số chức năng của bạn --- có nghĩa là: nếu bạn dự định sửa đổi giá trị đối số bằng cách sao chép nó vào một biến nội bộ, chỉ cần sử dụng một đối số giá trị thay thế .

Vì vậy, đừng làm điều này :

std::string function(const std::string& aString){
    auto vString(aString);
    vString.clear();
    return vString;
}

làm điều này :

std::string function(std::string aString){
    aString.clear();
    return aString;
}

Khi bạn cần sửa đổi giá trị đối số trong thân hàm.

Bạn chỉ cần nhận thức được cách bạn dự định sử dụng đối số trong thân hàm. Chỉ đọc hoặc KHÔNG ... và nếu nó nằm trong phạm vi.


2
Bạn khuyên bạn nên chuyển qua tham chiếu trong một số trường hợp, nhưng bạn chỉ ra một hướng dẫn khuyến nghị luôn luôn vượt qua giá trị.
Keith Thompson

3
@KeithThndry Đừng sao chép đối số chức năng của bạn. Có nghĩa là không sao chép const ref&vào một biến nội bộ để sửa đổi nó. Nếu bạn cần sửa đổi nó ... làm cho tham số trở thành một giá trị. Nó khá rõ ràng đối với bản thân không nói tiếng Anh của tôi.
CodeAngry

5
@KeithThndry Trích dẫn Hướng dẫn (Không sao chép các đối số chức năng của bạn. Thay vào đó, hãy chuyển chúng theo giá trị và để trình biên dịch thực hiện sao chép.) Được sao chép từ trang đó. Nếu điều đó không đủ rõ ràng, tôi không thể giúp. Tôi không hoàn toàn tin tưởng trình biên dịch để đưa ra lựa chọn tốt nhất. Tôi muốn nói rõ hơn về ý định của tôi trong cách tôi xác định các đối số hàm. # 1 Nếu chỉ đọc, nó là a const ref&. # 2 Nếu tôi cần viết nó hoặc tôi biết nó nằm ngoài phạm vi ... Tôi sử dụng một giá trị. # 3 Nếu tôi cần sửa đổi giá trị ban đầu, tôi sẽ chuyển qua ref&. # 4 Tôi sử dụng pointers *nếu một đối số là tùy chọn để tôi có thể nullptr.
CodeAngry

11
Tôi không đứng về phía câu hỏi liệu có nên vượt qua giá trị hay bằng cách tham khảo. Quan điểm của tôi là bạn ủng hộ việc chuyển qua tham chiếu trong một số trường hợp, nhưng sau đó trích dẫn (dường như để hỗ trợ vị trí của bạn) một hướng dẫn khuyến nghị luôn luôn vượt qua giá trị. Nếu bạn không đồng ý với hướng dẫn, bạn có thể muốn nói như vậy và giải thích lý do. (Các liên kết đến cpp-next.com không hoạt động với tôi.)
Keith Thompson

4
@KeithThndry: Bạn đang diễn giải sai hướng dẫn. Nó không phải là "luôn luôn" vượt qua giá trị. Tóm lại, đó là "Nếu bạn đã tạo một bản sao cục bộ, hãy sử dụng pass by value để trình biên dịch thực hiện bản sao đó cho bạn." Bạn không nên sử dụng pass-by-value khi bạn không tạo một bản sao.
Ben Voigt

43

Trừ khi bạn thực sự cần một bản sao, nó vẫn hợp lý để lấy const &. Ví dụ:

bool isprint(std::string const &s) {
    return all_of(begin(s),end(s),(bool(*)(char))isprint);
}

Nếu bạn thay đổi chuỗi này để lấy chuỗi theo giá trị thì cuối cùng bạn sẽ di chuyển hoặc sao chép tham số và không cần điều đó. Không chỉ là sao chép / di chuyển có thể đắt hơn, mà còn giới thiệu một thất bại tiềm năng mới; việc sao chép / di chuyển có thể tạo ra một ngoại lệ (ví dụ: phân bổ trong khi sao chép có thể không thành công) trong khi việc tham chiếu đến một giá trị hiện tại thì không thể.

Nếu bạn làm cần một bản sao sau đó đi qua và trở lại theo giá trị là thường (luôn?) Lựa chọn tốt nhất. Trong thực tế, tôi thường không lo lắng về điều đó trong C ++ 03 trừ khi bạn thấy rằng các bản sao thêm thực sự gây ra vấn đề về hiệu suất. Sao chép có vẻ khá đáng tin cậy trên trình biên dịch hiện đại. Tôi nghĩ rằng sự hoài nghi và khăng khăng của mọi người rằng bạn phải kiểm tra bảng hỗ trợ trình biên dịch cho RVO hiện nay đã lỗi thời.


Nói tóm lại, C ++ 11 không thực sự thay đổi bất cứ điều gì về vấn đề này ngoại trừ những người không tin tưởng vào cuộc bầu cử sao chép.


2
Các constructor di chuyển thường được triển khai cùng noexcept, nhưng rõ ràng các constructor không có.
leftaroundabout

25

Hầu hết.

Trong C ++ 17, chúng ta có basic_string_view<?>, điều này đưa chúng ta đến một trường hợp sử dụng hẹp chostd::string const& các tham số.

Sự tồn tại của ngữ nghĩa di chuyển đã loại bỏ một trường hợp sử dụng cho std::string const&- nếu bạn đang dự định lưu trữ tham số, lấy một std::stringgiá trị là tối ưu hơn, như bạn có thểmove thoát khỏi tham số.

Nếu ai đó gọi hàm của bạn bằng C thô, "string"điều này có nghĩa là chỉ có một std::stringbộ đệm được phân bổ, trái ngược với hai trong std::string const&trường hợp.

Tuy nhiên, nếu bạn không có ý định tạo một bản sao, việc sử dụng std::string const&vẫn hữu ích trong C ++ 14.

Với std::string_view, miễn là bạn không chuyển chuỗi đã nói đến API mong đợi '\0'bộ đệm ký tự được hủy theo kiểu C , bạn có thể có được std::stringchức năng như hiệu quả hơn mà không phải chịu bất kỳ phân bổ nào. Một chuỗi C thô thậm chí có thể được biến thành một chuỗi std::string_viewmà không có sự phân bổ hoặc sao chép ký tự.

Tại thời điểm đó, việc sử dụng std::string const&là khi bạn không sao chép dữ liệu bán buôn và sẽ chuyển nó sang API kiểu C, mong đợi bộ đệm kết thúc null và bạn cần các hàm chuỗi cấp cao hơn std::stringcung cấp. Trong thực tế, đây là một bộ yêu cầu hiếm.


2
Tôi đánh giá cao câu trả lời này - nhưng tôi muốn chỉ ra rằng nó bị ảnh hưởng (như nhiều câu trả lời chất lượng làm) từ một chút sai lệch cụ thể của tên miền. Nói một cách dí dỏm: Trong thực tế, đây là một tập hợp các yêu cầu hiếm hoi trong kinh nghiệm phát triển của riêng tôi, những hạn chế này - dường như bị thu hẹp một cách bất thường đối với tác giả - luôn luôn được đáp ứng. Thật đáng để chỉ ra điều này.
cá2000

1
@ fish2000 Để rõ ràng, std::stringđể thống trị bạn không chỉ cần một số yêu cầu đó mà là tất cả chúng. Bất kỳ một hoặc thậm chí hai trong số đó, tôi đều thừa nhận, phổ biến. Có lẽ bạn thường cần cả 3 (như, bạn đang thực hiện phân tích cú pháp đối số chuỗi để chọn API C nào bạn sẽ chuyển qua bán buôn cho?)
Yakk - Adam Nevraumont

@ Yakk-AdamNevraumont Đây là một điều YMMV - nhưng đó là trường hợp sử dụng thường xuyên nếu (giả sử) bạn đang lập trình chống lại POSIX hoặc các API khác trong đó ngữ nghĩa chuỗi C, như mẫu số chung thấp nhất. Tôi nên nói thực sự rằng tôi yêu std::string_view- như bạn chỉ ra, chuỗi Một chuỗi C thô thậm chí có thể được biến thành std :: string_view mà không cần phân bổ hoặc sao chép ký tự, đó là điều đáng để nhớ với những người đang sử dụng C ++ trong ngữ cảnh sử dụng API như vậy, thực sự.
cá2000

1
@ fish2000 "'Một chuỗi C thô thậm chí có thể được chuyển thành std :: string_view mà không cần phân bổ hoặc sao chép ký tự', đây là điều đáng để ghi nhớ". Thật vậy, nhưng nó bỏ đi phần tốt nhất-- trong trường hợp chuỗi thô là một chuỗi ký tự, nó thậm chí không yêu cầu strlen () !
Don nở

17

std::stringkhông phải là dữ liệu cũ đơn giản (POD) và kích thước thô của nó không phải là điều phù hợp nhất từng có. Ví dụ: nếu bạn truyền vào một chuỗi cao hơn độ dài của SSO và được phân bổ trên heap, tôi sẽ mong đợi hàm tạo sao chép không sao chép bộ lưu trữ SSO.

Lý do điều này được khuyến nghị là vì invalđược xây dựng từ biểu thức đối số và do đó luôn được di chuyển hoặc sao chép khi thích hợp - không có mất hiệu suất, giả sử rằng bạn cần quyền sở hữu đối số. Nếu bạn không, một consttài liệu tham khảo vẫn có thể là cách tốt hơn để đi.


2
Điểm thú vị về việc người xây dựng bản sao đủ thông minh để không lo lắng về SSO nếu nó không sử dụng nó. Có lẽ đúng, tôi sẽ phải kiểm tra xem đó là sự thật ;-)
Benj

3
@Benj: Nhận xét cũ tôi biết, nhưng nếu SSO đủ nhỏ sao chép thì vô điều kiện sẽ nhanh hơn làm một nhánh có điều kiện. Ví dụ, 64 byte là một dòng bộ đệm và có thể được sao chép trong một khoảng thời gian thực sự nhỏ. Có thể là 8 chu kỳ hoặc ít hơn trên x86_64.
Zan Lynx

Ngay cả khi SSO không được sao chép bởi hàm tạo sao chép, std::string<>thì 32 byte được phân bổ từ ngăn xếp, 16 trong số đó cần được khởi tạo. So sánh điều này với chỉ 8 byte được phân bổ và khởi tạo cho một tham chiếu: Nó gấp đôi lượng CPU làm việc và nó chiếm gấp bốn lần dung lượng bộ nhớ cache không có sẵn cho các dữ liệu khác.
cmaster - phục hồi monica

Ồ, và tôi đã quên nói về việc truyền các đối số hàm trong các thanh ghi; điều đó sẽ làm giảm mức sử dụng ngăn xếp của tham chiếu xuống 0 cho lần cuối cùng ...
cmaster - khôi phục monica

16

Tôi đã sao chép / dán câu trả lời từ câu hỏi này tại đây và thay đổi tên và chính tả để phù hợp với câu hỏi này.

Đây là mã để đo những gì đang được hỏi:

#include <iostream>

struct string
{
    string() {}
    string(const string&) {std::cout << "string(const string&)\n";}
    string& operator=(const string&) {std::cout << "string& operator=(const string&)\n";return *this;}
#if (__has_feature(cxx_rvalue_references))
    string(string&&) {std::cout << "string(string&&)\n";}
    string& operator=(string&&) {std::cout << "string& operator=(string&&)\n";return *this;}
#endif

};

#if PROCESS == 1

string
do_something(string inval)
{
    // do stuff
    return inval;
}

#elif PROCESS == 2

string
do_something(const string& inval)
{
    string return_val = inval;
    // do stuff
    return return_val; 
}

#if (__has_feature(cxx_rvalue_references))

string
do_something(string&& inval)
{
    // do stuff
    return std::move(inval);
}

#endif

#endif

string source() {return string();}

int main()
{
    std::cout << "do_something with lvalue:\n\n";
    string x;
    string t = do_something(x);
#if (__has_feature(cxx_rvalue_references))
    std::cout << "\ndo_something with xvalue:\n\n";
    string u = do_something(std::move(x));
#endif
    std::cout << "\ndo_something with prvalue:\n\n";
    string v = do_something(source());
}

Đối với tôi kết quả này:

$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=1 test.cpp
$ a.out
do_something with lvalue:

string(const string&)
string(string&&)

do_something with xvalue:

string(string&&)
string(string&&)

do_something with prvalue:

string(string&&)
$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp
$ a.out
do_something with lvalue:

string(const string&)

do_something with xvalue:

string(string&&)

do_something with prvalue:

string(string&&)

Bảng dưới đây tóm tắt kết quả của tôi (sử dụng clang -std = c ++ 11). Số thứ nhất là số công trình sao chép và số thứ hai là số công trình di chuyển:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |
+----+--------+--------+---------+
| p1 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p2 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+

Giải pháp vượt qua giá trị chỉ cần một lần quá tải nhưng chi phí xây dựng di chuyển thêm khi chuyển giá trị và giá trị x. Điều này có thể hoặc không thể được chấp nhận cho bất kỳ tình huống nào. Cả hai giải pháp đều có ưu điểm và nhược điểm.


1
std :: string là một lớp thư viện chuẩn. Nó đã có thể di chuyển và có thể sao chép. Tôi không thấy làm thế nào điều này có liên quan. OP đang hỏi nhiều hơn về hiệu suất di chuyển so với tham chiếu , chứ không phải hiệu suất di chuyển so với sao chép.
Nicol Bolas

3
Câu trả lời này đếm số lần di chuyển và sao chép chuỗi std :: sẽ trải qua thiết kế theo giá trị được mô tả bởi cả Herb và Dave, so với chuyển qua tham chiếu với một cặp hàm quá tải. Tôi sử dụng mã của OP trong bản demo, ngoại trừ việc thay thế bằng một chuỗi giả để hét lên khi nó được sao chép / di chuyển.
Howard Hinnant

Có lẽ bạn nên tối ưu hóa mã trước khi thực hiện các bài kiểm tra ...
Các thuận Croissant

3
@TheParamag từCroissant: Bạn đã nhận được kết quả khác nhau? Nếu vậy, sử dụng trình biên dịch với các đối số dòng lệnh nào?
Howard Hinnant

14

Herb Sutter vẫn đang được ghi nhận, cùng với Bjarne Stroustroup, trong khuyến nghị const std::string&là một loại tham số; xem https://github.com/isocpp/CppCoreGuiances/blob/master/CppCoreGuiances.md#Rf-in .

Có một cạm bẫy không được đề cập trong bất kỳ câu trả lời nào khác ở đây: nếu bạn chuyển một chuỗi ký tự cho một const std::string&tham số, nó sẽ chuyển một tham chiếu đến một chuỗi tạm thời, được tạo ra để giữ các ký tự của chữ. Nếu sau đó bạn lưu tham chiếu đó, nó sẽ không hợp lệ khi chuỗi tạm thời bị hủy. Để an toàn, bạn phải lưu một bản sao , không phải tài liệu tham khảo. Vấn đề bắt nguồn từ thực tế là chuỗi ký tự là const char[N]loại, yêu cầu quảng bá std::string.

Mã dưới đây minh họa cạm bẫy và cách giải quyết, cùng với một tùy chọn hiệu quả nhỏ - quá tải với một const char*phương thức, như được mô tả tại Có cách nào để truyền một chuỗi ký tự như tham chiếu trong C ++ .

(Lưu ý: Sutter & Stroustroup khuyên rằng nếu bạn giữ một bản sao của chuỗi, cũng cung cấp một hàm quá tải với tham số && và std :: move () nó.)

#include <string>
#include <iostream>
class WidgetBadRef {
public:
    WidgetBadRef(const std::string& s) : myStrRef(s)  // copy the reference...
    {}

    const std::string& myStrRef;    // might be a reference to a temporary (oops!)
};

class WidgetSafeCopy {
public:
    WidgetSafeCopy(const std::string& s) : myStrCopy(s)
            // constructor for string references; copy the string
    {std::cout << "const std::string& constructor\n";}

    WidgetSafeCopy(const char* cs) : myStrCopy(cs)
            // constructor for string literals (and char arrays);
            // for minor efficiency only;
            // create the std::string directly from the chars
    {std::cout << "const char * constructor\n";}

    const std::string myStrCopy;    // save a copy, not a reference!
};

int main() {
    WidgetBadRef w1("First string");
    WidgetSafeCopy w2("Second string"); // uses the const char* constructor, no temp string
    WidgetSafeCopy w3(w2.myStrCopy);    // uses the String reference constructor
    std::cout << w1.myStrRef << "\n";   // garbage out
    std::cout << w2.myStrCopy << "\n";  // OK
    std::cout << w3.myStrCopy << "\n";  // OK
}

ĐẦU RA:

const char * constructor
const std::string& constructor

Second string
Second string

Đây là một vấn đề khác và WidgetBadRef không cần phải có tham số & tham số sai. Câu hỏi đặt ra là nếu WidgetSafeCopy chỉ lấy một tham số chuỗi thì nó có chậm hơn không? (Tôi nghĩ rằng bản sao tạm thời cho thành viên chắc chắn dễ dàng phát hiện hơn)
Superfly Jon

Lưu ý rằng T&&không phải lúc nào cũng là một tài liệu tham khảo phổ quát; trong thực tế, std::string&&sẽ luôn luôn là một tham chiếu giá trị, và không bao giờ là một tham chiếu phổ quát, bởi vì không có loại trừ nào được thực hiện . Do đó, lời khuyên của Stroustroup & Sutter không mâu thuẫn với Meyers '.
Thời gian của Justin - Phục hồi lại

@JustinTime: cảm ơn bạn; Tôi đã xóa câu cuối cùng không chính xác, trong thực tế, std :: string && sẽ là một tài liệu tham khảo phổ quát.
circlepi314

@ circlepi314 Bạn được chào đón. Đó là một kết hợp dễ dàng để thực hiện, đôi khi có thể gây nhầm lẫn cho dù bất kỳ được đưa ra T&&là một tham chiếu phổ quát suy diễn hoặc một tham chiếu rvalue không suy diễn; có lẽ mọi thứ sẽ rõ ràng hơn nếu họ giới thiệu một biểu tượng khác cho các tài liệu tham khảo phổ quát (như là &&&sự kết hợp của &&&), nhưng điều đó có lẽ trông thật ngớ ngẩn.
Thời gian của Justin - Phục hồi Monica

8

IMO sử dụng tham chiếu C ++ cho việc std::stringtối ưu hóa cục bộ nhanh và ngắn, trong khi sử dụng chuyển theo giá trị có thể (hoặc không) tối ưu hóa toàn cầu tốt hơn.

Vì vậy, câu trả lời là: nó phụ thuộc vào hoàn cảnh:

  1. Nếu bạn viết tất cả mã từ bên ngoài vào các hàm bên trong, bạn sẽ biết mã đó làm gì, bạn có thể sử dụng tham chiếu const std::string &.
  2. Nếu bạn viết mã thư viện hoặc sử dụng nhiều mã thư viện nơi các chuỗi được truyền, bạn có thể đạt được nhiều hơn theo nghĩa toàn cầu bằng cách tin tưởng std::stringhành vi của nhà xây dựng sao chép.

6

Xem bài viết của Herb Herb Sutter "Trở lại những điều cơ bản! Những điều cốt yếu của Phong cách C ++ hiện đại . Trong số các chủ đề khác, anh ấy xem xét thông số đưa ra lời khuyên đã được đưa ra trong quá khứ và những ý tưởng mới có trong C ++ 11 và đặc biệt xem xét ý tưởng truyền chuỗi theo giá trị.

trượt 24

Các điểm chuẩn cho thấy rằng việc truyền std::strings theo giá trị, trong trường hợp hàm sẽ sao chép nó bằng mọi cách, có thể chậm hơn đáng kể!

Điều này là do bạn buộc nó phải luôn tạo một bản sao đầy đủ (và sau đó di chuyển vào vị trí), trong khi const&phiên bản sẽ cập nhật chuỗi cũ có thể sử dụng lại bộ đệm đã được phân bổ.

Xem slide 27 của anh ấy: Đối với các chức năng của bộ cài đặt, các tùy chọn 1 giống như mọi khi. Tùy chọn 2 thêm một quá tải cho tham chiếu giá trị, nhưng điều này mang lại một vụ nổ tổ hợp nếu có nhiều tham số.

Chỉ dành cho các tham số của chìm chìm trên mạng, nơi một chuỗi phải được tạo (không thay đổi giá trị hiện tại của nó) mà thủ thuật truyền qua giá trị là hợp lệ. Đó là, các hàm tạo trong đó tham số khởi tạo trực tiếp thành viên của kiểu khớp.

Nếu bạn muốn xem bạn có thể lo lắng về vấn đề này sâu đến mức nào, hãy xem bài thuyết trình của Nicolai Josuttis và chúc may mắn với điều đó (Tiết hoàn hảo - Xong! Lần n sau khi tìm thấy lỗi với phiên bản trước. Bạn đã từng ở đó chưa?)


Điều này cũng được tóm tắt là .15F.15 trong Nguyên tắc chuẩn.


3

Như @ JDługosz chỉ ra trong các bình luận, Herb đưa ra lời khuyên khác trong một cuộc nói chuyện khác (sau này?), Xem đại khái từ đây: https://youtu.be/xnqTKD8uD64?t=54m50s .

Lời khuyên của anh ta chỉ rút gọn khi chỉ sử dụng các tham số giá trị cho một hàm flấy cái gọi là đối số chìm, giả sử bạn sẽ di chuyển cấu trúc từ các đối số chìm này.

Cách tiếp cận chung này chỉ thêm chi phí chung của một hàm tạo di chuyển cho cả đối số lvalue và rvalue so với triển khai tối ưu của các fđối số lvalue và rvalue tương ứng. Để xem tại sao lại như vậy, giả sử flấy một tham số giá trị, trong đó Tmột số bản sao và di chuyển loại có thể xây dựng:

void f(T x) {
  T y{std::move(x)};
}

Gọi fvới một đối số lvalue sẽ dẫn đến một hàm tạo sao chép được gọi để xây dựng xvà một hàm tạo di chuyển được gọi để xây dựng y. Mặt khác, việc gọi fvới một đối số giá trị sẽ khiến một hàm tạo di chuyển được gọi để xây dựng xvà một hàm tạo di chuyển khác được gọi để xây dựng y.

Nói chung, việc triển khai tối ưu fcho các đối số lvalue như sau:

void f(const T& x) {
  T y{x};
}

Trong trường hợp này, chỉ có một hàm tạo sao chép được gọi để xây dựng y. Việc triển khai tối ưu fcho các đối số giá trị là, nói chung, như sau:

void f(T&& x) {
  T y{std::move(x)};
}

Trong trường hợp này, chỉ có một hàm tạo di chuyển được gọi để xây dựng y.

Vì vậy, một sự thỏa hiệp hợp lý là lấy một tham số giá trị và có thêm một lệnh gọi hàm tạo di chuyển bổ sung cho các đối số giá trị hoặc giá trị liên quan đến việc triển khai tối ưu, đó cũng là lời khuyên được đưa ra trong bài nói chuyện của Herb.

Như @ JDługosz đã chỉ ra trong các bình luận, việc truyền theo giá trị chỉ có ý nghĩa đối với các hàm sẽ xây dựng một số đối tượng từ đối số chìm. Khi bạn có một chức năng fsao chép đối số của nó, cách tiếp cận thông qua giá trị sẽ có nhiều chi phí hơn so với cách tiếp cận tham chiếu thông qua chung. Cách tiếp cận pass-by-value cho một hàm fgiữ một bản sao của tham số của nó sẽ có dạng:

void f(T x) {
  T y{...};
  ...
  y = std::move(x);
}

Trong trường hợp này, có một cấu trúc sao chép và gán di chuyển cho một đối số giá trị, và một chuyển nhượng xây dựng và di chuyển cho một đối số giá trị. Trường hợp tối ưu nhất cho một đối số lvalue là:

void f(const T& x) {
  T y{...};
  ...
  y = x;
}

Điều này chỉ rút gọn thành một nhiệm vụ, có khả năng rẻ hơn nhiều so với hàm tạo sao chép cộng với chuyển nhượng cần thiết cho cách tiếp cận giá trị truyền qua. Lý do cho điều này là việc gán có thể sử dụng lại bộ nhớ được phân bổ hiện có yvà do đó ngăn chặn (de) phân bổ, trong khi đó, hàm tạo sao chép thường sẽ phân bổ bộ nhớ.

Đối với một đối số giá trị, việc triển khai tối ưu nhất cho fviệc giữ lại một bản sao có dạng:

void f(T&& x) {
  T y{...};
  ...
  y = std::move(x);
}

Vì vậy, chỉ có một nhiệm vụ di chuyển trong trường hợp này. Việc chuyển một giá trị cho phiên bản flấy tham chiếu const chỉ tốn một nhiệm vụ thay vì chỉ định di chuyển. Vì vậy, tương đối mà nói, phiên bản flấy tham chiếu const trong trường hợp này là triển khai chung là thích hợp hơn.

Vì vậy, nói chung, để thực hiện tối ưu nhất, bạn sẽ cần quá tải hoặc thực hiện một số loại chuyển tiếp hoàn hảo như trong bài nói chuyện. Hạn chế là một vụ nổ tổ hợp về số lượng quá tải cần thiết, tùy thuộc vào số lượng tham số ftrong trường hợp bạn chọn quá tải trên danh mục giá trị của đối số. Chuyển tiếp hoàn hảo có nhược điểm ftrở thành chức năng mẫu, điều này ngăn việc biến nó thành ảo và dẫn đến mã phức tạp hơn đáng kể nếu bạn muốn làm cho đúng 100% (xem phần thảo luận để biết chi tiết chính).


Xem kết quả của Herb Sutter trong câu trả lời mới của tôi: chỉ làm điều đó khi bạn di chuyển xây dựng , không di chuyển gán.
JDługosz

1
@ JDługosz, cảm ơn con trỏ đến cuộc nói chuyện của Herb, tôi chỉ xem nó và sửa đổi hoàn toàn câu trả lời của tôi. Tôi đã không nhận thức được (di chuyển) lời khuyên.
Ton van den Heuvel

con số và lời khuyên đó hiện có trong tài liệu Nguyên tắc chuẩn .
JDługosz

1

Vấn đề là "const" là vòng loại không hạt. Điều thường có nghĩa là "const chuỗi ref" là "không sửa đổi chuỗi này", không phải "không sửa đổi số tham chiếu". Đơn giản là không có cách nào, trong C ++, để nói thành viên là "const". Họ hoặc là tất cả, hoặc không ai trong số họ là.

Để hack xung quanh vấn đề ngôn ngữ này, STL có thể cho phép "C ()" trong ví dụ của bạn tạo một bản sao ngữ nghĩa di chuyển bằng mọi cách , và bỏ qua "const" liên quan đến số tham chiếu (có thể thay đổi). Miễn là nó được chỉ định rõ, điều này sẽ ổn.

Vì STL không có, tôi có một phiên bản của chuỗi cấu thành <> bộ đếm tham chiếu (không có cách nào để tạo ra một thứ gì đó có thể thay đổi trong hệ thống phân cấp lớp) và - lo và kìa - bạn có thể tự do chuyển các chuỗi của chuỗi như tham chiếu const, và tạo các bản sao của chúng trong các chức năng sâu, suốt cả ngày, không có rò rỉ hoặc vấn đề.

Vì C ++ không cung cấp "mức độ chi tiết const constity" ở đây, nên viết ra một đặc tả kỹ thuật tốt và tạo một đối tượng "const Movable string" (cm Chuỗi) mới sáng bóng là giải pháp tốt nhất tôi từng thấy.


@BenVoigt yep ... thay vì bỏ đi, nó sẽ có thể thay đổi ... nhưng bạn không thể thay đổi thành viên STL thành có thể thay đổi trong lớp dẫn xuất.
Erik Aronesty
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.