2015-07-24 21 views
7

Tôi là một người mới đến C++ và tôi gặp phải một vấn đề gần đây trở về một tham chiếu đến một biến địa phương. Tôi đã giải quyết nó bằng cách thay đổi giá trị trả lại từ std::string& thành số std::string. Tuy nhiên, với sự hiểu biết của tôi, điều này có thể rất kém hiệu quả. Xét đoạn mã sau:Làm thế nào để tránh sao chép-xây dựng một giá trị trả về

string hello() 
{ 
    string result = "hello"; 
    return result; 
} 

int main() 
{ 
    string greeting = hello(); 
} 

Để hiểu biết của tôi, những gì xảy ra là:

  • hello() được gọi.
  • Biến cục bộ result được gán giá trị là "hello".
  • Giá trị của result được sao chép vào biến greeting.

Điều này có lẽ không quan trọng mà nhiều cho std::string, nhưng nó chắc chắn có thể tốn kém nếu bạn có, ví dụ, một bảng băm với hàng trăm mục.

Làm cách nào để tránh sao chép-xây dựng tạm thời trả về và thay vào đó trả lại bản sao của con trỏ tới đối tượng (về cơ bản, bản sao của biến cục bộ)?


Sidenote:. Tôi đã heard rằng trình biên dịch đôi khi sẽ thực hiện tối ưu hóa lợi nhuận có giá trị để tránh gọi constructor sao chép, nhưng tôi nghĩ rằng tốt nhất là không phải dựa vào các tối ưu hóa trình biên dịch để làm cho chạy mã của bạn một cách hiệu quả)

+1

Không tối ưu hóa cho đến khi bạn xác nhận rằng giải pháp của bạn không đáp ứng được các yêu cầu kinh doanh và sau đó chỉ tối ưu hóa những gì mà trình lược tả cho bạn biết là quá chậm. Hãy để trình biên dịch xử lý nó. Nó sẽ dễ dàng hơn để đọc và duy trì. –

+2

Trong C++ 11 điều này gần như chắc chắn sẽ sử dụng một nhà xây dựng di chuyển nếu vì một số lý do RVO không thành công. Nếu bạn muốn hoàn toàn chắc chắn, vượt qua chuỗi đầu ra trong tham chiếu thay vì trả lại nó. RVO là một phần của tiêu chuẩn bây giờ mặc dù vậy miễn là bạn có một trình biên dịch hiện đại, bạn thường có thể dựa vào nó. –

+0

@PeteBaughman và Jonathan Potter: Tôi nhận ra rằng trình biên dịch có thể tối ưu hóa điều này, và điều này có thể không gây ra chi phí lớn trong ứng dụng của tôi, nhưng không thể thực hiện điều này một cách cơ bản theo nghĩa? –

Trả lời

9

Mô tả trong câu hỏi của bạn khá chính xác. Nhưng điều quan trọng là phải hiểu rằng đây là hành vi của máy C++ trừu tượng .Trong thực tế, mô tả kinh điển của hành vi trả lại trừu tượng thậm chí còn ít hơn tối ưu

  1. result được sao chép vào một đối tượng tạm thời trung gian không tên của loại std::string. Tạm thời đó vẫn tồn tại sau khi trở lại chức năng.
  2. Đối tượng tạm thời trung gian vô danh đó sau đó được sao chép sang greeting sau khi trả về hàm.

Hầu hết các trình biên dịch luôn đủ thông minh để loại bỏ tạm thời trung gian đó theo đúng quy tắc phân chia sao chép cổ điển. Nhưng ngay cả khi không có trung gian tạm thời, hành vi này luôn được xem là kém tối ưu. Đó là lý do tại sao rất nhiều quyền tự do được trao cho các trình biên dịch để cung cấp cho họ cơ hội tối ưu hóa trong các ngữ cảnh trả về giá trị. Ban đầu nó đã được tối ưu hóa giá trị trả lại (RVO). Sau này được đặt tên là Tối ưu hóa giá trị trả lại được thêm vào nó (NRVO). Và cuối cùng, trong C++ 11, di chuyển ngữ nghĩa đã trở thành một cách bổ sung để tối ưu hóa hành vi trả về trong các trường hợp như vậy.

Lưu ý rằng dưới NRVO trong ví dụ của bạn khởi tạo của result với "hello" thực sự đặt rằng "hello" trực tiếp vào greeting ngay từ đầu.

Vì vậy, trong C++ hiện đại, lời khuyên tốt nhất là: để nguyên như vậy và đừng tránh nó. Trả lại theo giá trị. (Và thích sử dụng ngay lập tức khởi tạo tại thời điểm khai báo bất cứ khi nào bạn có thể, thay vì chọn khởi tạo mặc định theo sau chuyển nhượng.)

Thứ nhất, khả năng của trình biên dịch /() sẽ loại bỏ việc sao chép. Trong bất kỳ trình biên dịch tự tôn trọng RVO/NRVO không phải là một cái gì đó tối nghĩa hoặc thứ cấp. Đó là điều gì đó mà các nhà văn biên dịch tích cực cố gắng thực hiện và thực hiện đúng cách.

Thứ hai, luôn có ngữ nghĩa di chuyển làm giải pháp dự phòng nếu RVO/NRVO bằng cách nào đó không thành công hoặc không áp dụng được. Di chuyển tự nhiên được áp dụng trong các ngữ cảnh trả về giá trị và nó rẻ hơn nhiều so với việc sao chép toàn diện cho các đối tượng không tầm thường. Và std::string là loại di chuyển.

+1

Để giải thích một phần của câu trả lời này, tôi đồng ý nhất; nó không đồng ý với sidenote trong câu hỏi đó là trong thực tế tốt để dựa vào một số tối ưu hóa trình biên dịch. – kasterma

+0

vâng vâng vâng có một triệu lần có –

3

có rất nhiều cách để đạt được điều đó:

1) Quay trở lại một số dữ liệu bằng cách tham chiếu

void SomeFunc(std::string& sResult) 
{ 
    sResult = "Hello world!"; 
} 

2) Re chuyển con trỏ đến đối tượng

CSomeHugeClass* SomeFunc() 
{ 
    CSomeHugeClass* pPtr = new CSomeHugeClass(); 
    //... 
    return(pPtr); 
} 

3) C++ 11 có thể sử dụng hàm khởi tạo trong trường hợp này. Xem thisthisthis để biết thêm thông tin.

4

Tôi không đồng ý với câu "Tôi nghĩ tốt nhất là không nên dựa vào tối ưu hóa trình biên dịch để làm cho mã của bạn hoạt động hiệu quả". Đó là cơ bản toàn bộ công việc của trình biên dịch. Công việc của bạn là viết mã nguồn rõ ràng, chính xác và có thể duy trì được. Đối với mọi vấn đề hiệu suất mà tôi từng phải khắc phục, tôi đã phải sửa chữa một trăm vấn đề trở lên do nhà phát triển cố gắng thông minh thay vì thực hiện điều gì đó đơn giản, chính xác và có thể bảo trì.

Hãy xem một số điều bạn có thể làm để cố gắng "giúp" trình biên dịch và xem chúng ảnh hưởng như thế nào đến khả năng bảo trì của mã nguồn.

  • Bạn có thể trả lại dữ liệu thông qua tham khảo

Ví dụ:

void hello(std::string& outString) 

Trở dữ liệu sử dụng một tài liệu tham khảo làm cho mã tại các cuộc gọi tại chỗ khó đọc.Nó gần như không thể nói những gì gọi là trạng thái đột biến như là một tác dụng phụ và không. Ngay cả khi bạn thực sự cẩn thận với const đủ điều kiện tham khảo nó sẽ khó đọc tại trang web cuộc gọi. Hãy xem xét ví dụ sau:

void hello(std::string& outString); //<-This one could modify outString 
void out(const std::string& toWrite); //<-This one definitely doesn't. 

. . . 

std::string myString; 
hello(myString); //<-This one maybe mutates myString - hard to tell. 
out(myString); //<-This one certainly doesn't, but it looks identical to the one above 

Thậm chí tuyên bố chào không rõ ràng. Liệu nó sửa đổi outString, hoặc là tác giả chỉ cẩu thả và quên const đủ điều kiện tham chiếu? Mã được viết trong một functional style dễ đọc và dễ hiểu hơn để vô tình phá vỡ.

Tránh

  • Bạn có thể trả về một con trỏ đến đối tượng thay vì trả lại đối tượng.

Trả về một con trỏ đến đối tượng khiến bạn khó chắc chắn mã của mình là chính xác. Trừ khi bạn sử dụng unique_ptr, bạn phải tin tưởng rằng bất cứ ai sử dụng phương pháp của bạn là kỹ lưỡng và chắc chắn để xóa con trỏ khi họ đang thực hiện với nó, nhưng đó không phải là rất RAII. std :: string đã là một loại trình bao bọc RAII cho một char * tóm tắt các vấn đề về tuổi thọ dữ liệu liên quan đến việc trả về một con trỏ. Trả về một con trỏ tới một std :: string chỉ giới thiệu lại các vấn đề mà std :: string được thiết kế để giải quyết. Dựa vào một con người để được siêng năng và đọc kỹ các tài liệu cho chức năng của bạn và biết khi nào để xóa con trỏ và khi không xóa con trỏ thì không có kết quả tích cực.

Tránh

  • đó mang lại cho chúng ta di chuyển nhà xây dựng.

Một hàm tạo di chuyển sẽ chỉ chuyển quyền sở hữu dữ liệu được trỏ đến từ 'kết quả' đến đích cuối cùng của nó. Sau đó, việc truy cập đối tượng 'kết quả' không hợp lệ nhưng điều đó không quan trọng - phương thức của bạn đã kết thúc và đối tượng 'kết quả' nằm ngoài phạm vi. Không sao chép, chỉ cần chuyển quyền sở hữu của con trỏ với ngữ nghĩa rõ ràng.

Thông thường trình biên dịch sẽ gọi hàm khởi tạo di chuyển cho bạn. Nếu bạn thực sự hoang tưởng (hoặc có kiến ​​thức cụ thể rằng trình biên dịch sẽ không giúp bạn), bạn có thể sử dụng std::move.

Thực hiện một trong này nếu có thể

Cuối cùng trình biên dịch hiện đại là tuyệt vời. Với trình biên dịch C++ hiện đại, 99% thời gian trình biên dịch sẽ thực hiện một số loại tối ưu hóa để loại bỏ bản sao. 1% thời gian khác có lẽ sẽ không thành vấn đề về hiệu suất. Trong những trường hợp cụ thể, trình biên dịch có thể viết lại một phương thức như std :: string GetString(); để void GetString (std :: string & outVar); tự động. Mã này vẫn còn dễ đọc, nhưng trong hội đồng cuối cùng bạn nhận được tất cả các lợi ích tốc độ thực hoặc tưởng tượng của trở lại bằng cách tham khảo. Đừng hy sinh khả năng đọc và bảo trì cho hiệu suất trừ khi bạn có kiến ​​thức cụ thể rằng giải pháp không đáp ứng các yêu cầu kinh doanh của bạn.

Các vấn đề liên quan