2012-05-07 36 views
20

Tôi có một câu hỏi rất cơ bản trong C++. Làm thế nào để tránh sao chép khi trả lại một đối tượng?C++: Tránh sao chép với câu trả lời "return"

Dưới đây là một ví dụ:

std::vector<unsigned int> test(const unsigned int n) 
{ 
    std::vector<unsigned int> x; 
    for (unsigned int i = 0; i < n; ++i) { 
     x.push_back(i); 
    } 
    return x; 
} 

Như tôi hiểu làm thế nào C++ hoạt động, chức năng này sẽ tạo ra 2 vectơ: Ai là địa phương (x), và các bản sao của x mà sẽ được trả lại. Có cách nào để tránh sao chép không? (và tôi không muốn trả lại con trỏ cho một đối tượng, nhưng chính đối tượng đó)

Cảm ơn bạn rất nhiều.

CHỈNH SỬA: câu hỏi phụ theo câu trả lời đầu tiên: cú pháp của hàm đó sử dụng "ngữ nghĩa di chuyển" là gì?

+0

di chuyển ngữ nghĩa: http://www2.research.att.com/~bs/C++0xFAQ.html#rval – chris

+0

Nó không nhất thiết phải tạo bản sao. NRVO hoặc di chuyển symantics có thể ngăn chặn điều đó. –

+1

Bạn có thể dựa vào trình biên dịch của mình để thực hiện phép thuật NRVO hoặc sử dụng ngữ nghĩa di chuyển một cách dễ dàng. –

Trả lời

14

Chương trình này có thể tận dụng tối ưu hóa giá trị trả lại được đặt tên (NRVO). Xem tại đây: http://en.wikipedia.org/wiki/Copy_elision

Trong C++ 11 có các hàm tạo và di chuyển cũng rẻ. Bạn có thể đọc hướng dẫn tại đây: http://thbecker.net/articles/rvalue_references/section_01.html

+1

Cần lưu ý rằng không phải tất cả các trình biên dịch sẽ làm điều này và thậm chí cả những trình biên dịch không phải lúc nào cũng vậy. Nó vẫn có thể là giá trị nhìn vào IFF đối tượng là lớn, bạn đang chú ý các bản sao, và hồ sơ cho thấy nó là một nút cổ chai đáng kể. –

13

Named Return Value Optimization sẽ thực hiện công việc cho bạn vì trình biên dịch sẽ cố gắng loại bỏ nhà xây dựng Sao chép dự phòng và các cuộc gọi Destructor trong khi sử dụng nó.

std::vector<unsigned int> test(const unsigned int n){ 
    std::vector<unsigned int> x; 
    return x; 
} 
... 
std::vector<unsigned int> y; 
y = test(10); 

với giá trị trả về tối ưu hóa:

  1. y được tạo ra
  2. x được tạo ra
  3. x được gán vào y
  4. x được destructed

(trong trường hợp bạn muốn tự mình tìm hiểu sâu hơn, hãy xem this example of mine)

hoặc thậm chí tốt hơn, giống như Matthieu M. chỉ ra, nếu bạn gọi test trong cùng một dòng nơi y được khai báo, bạn cũng có thể tránh xây dựng các đối tượng dư thừa và chuyển nhượng dự phòng cũng như (x sẽ được xây dựng trong bộ nhớ nơi y sẽ được lưu trữ):

std::vector<unsigned int> y = test(10); 

kiểm tra câu trả lời của mình cho hiểu rõ hơn về tình hình đó (bạn cũng sẽ tìm ra rằng loại tối ưu hóa không thể luôn luôn được áp dụng).

HOẶC bạn có thể sửa đổi mã của bạn để vượt qua tham chiếu của vector để chức năng của bạn, đó sẽ là ngữ nghĩa chính xác hơn trong khi tránh sao chép:

void test(std::vector<unsigned int>& x){ 
    // use x.size() instead of n 
    // do something with x... 
} 
... 
std::vector<unsigned int> y; 
test(y); 
+0

Ah tôi hiểu rồi. Thật vậy, tôi đã bỏ qua thực tế là bạn đã xây dựng một 'y' mặc định trước khi gán cho nó. Trình tự của các sự kiện là đúng trong trường hợp này, mặc dù tôi khuyên bạn chỉ nên khởi tạo trực tiếp 'y' để tránh tạo hai đối tượng trong đó một đối tượng sẽ đủ. Xin lỗi vì tiếng ồn. –

+1

@MatthieuM: Tôi đánh giá cao điểm của bạn. Kiểm tra câu trả lời của tôi ngay bây giờ :) – LihO

+2

Tôi không thể upvote hai lần: ( –

-5

Trước hết, bạn có thể khai báo kiểu trả về của bạn để được std :: vector & trong trường hợp đó một tham chiếu sẽ được trả về thay vì một bản sao.

Bạn cũng có thể xác định một con trỏ, tạo một con trỏ bên trong thân phương thức của bạn và sau đó trả về con trỏ đó (hoặc một bản sao của con trỏ đó là chính xác).

Cuối cùng, nhiều trình biên dịch C++ có thể thực hiện tối ưu hóa giá trị trả về (http://en.wikipedia.org/wiki/Return_value_optimization) loại bỏ đối tượng tạm thời trong một số trường hợp.

+4

Quá xấu tài liệu tham khảo sẽ ngay lập tức là bất hợp pháp để sử dụng (UB). -1 cho lời khuyên không hợp lệ – Mankarse

1

Tham chiếu nó sẽ hoạt động.

Void(vector<> &x) { 

} 
31

Dường như có sự nhầm lẫn về cách hoạt động của RVO (Tối ưu hóa giá trị trả lại).

Một ví dụ đơn giản:

#include <iostream> 

struct A { 
    int a; 
    int b; 
    int c; 
    int d; 
}; 

A create(int i) { 
    A a = {i, i+1, i+2, i+3 }; 
    std::cout << &a << "\n"; 
    return a; 
} 

int main(int argc, char*[]) { 
    A a = create(argc); 
    std::cout << &a << "\n"; 
} 

Và sản lượng của nó tại ideone:

0xbf928684 
0xbf928684 

ngạc nhiên?

Trên thực tế, đó là ảnh hưởng của RVO: đối tượng để được trả lại được xây dựng trực tiếp tại chỗ trong người gọi.

Làm cách nào?

Theo truyền thống, người gọi (main tại đây) sẽ đặt trước một số khoảng trống trên giá trị trả lại: khe trả về; callee (create ở đây) được chuyển (bằng cách nào đó) địa chỉ của khe trả về để sao chép giá trị trả về của nó vào. Sau đó, callee phân bổ không gian riêng của nó cho biến cục bộ, trong đó nó tạo kết quả, giống như cho bất kỳ biến cục bộ nào khác, và sau đó sao chép nó vào khe trả về sau câu lệnh return.

RVO được kích hoạt khi trình biên dịch suy ra từ mã mà biến có thể được xây dựng trực tiếp vào khe trả về với ngữ nghĩa tương đương (quy tắc as-if). Lưu ý rằng đây là một tối ưu hóa phổ biến mà nó được liệt kê rõ ràng bởi tiêu chuẩn và trình biên dịch không phải lo lắng về các tác dụng phụ có thể xảy ra của nhà xây dựng bản sao (hoặc di chuyển).

Khi nào?

Trình biên dịch rất có thể sử dụng quy tắc đơn giản, chẳng hạn như:

// 1. works 
A unnamed() { return {1, 2, 3, 4}; } 

// 2. works 
A unique_named() { 
    A a = {1, 2, 3, 4}; 
    return a; 
} 

// 3. works 
A mixed_unnamed_named(bool b) { 
    if (b) { return {1, 2, 3, 4}; } 

    A a = {1, 2, 3, 4}; 
    return a; 
} 

// 4. does not work 
A mixed_named_unnamed(bool b) { 
    A a = {1, 2, 3, 4}; 

    if (b) { return {4, 3, 2, 1}; } 

    return a; 
} 

Trong trường hợp sau (4), tối ưu hóa không thể được áp dụng khi A được trả về bởi vì trình biên dịch không thể xây dựng a trong trở lại khe, vì nó có thể cần nó cho cái gì khác (tùy thuộc vào điều kiện boolean b).

Một nguyên tắc đơn giản của ngón tay cái là như vậy đó:

RVO nên được áp dụng nếu không có ứng cử viên khác cho khe trở lại đã được công bố trước khi báo cáo kết quả return.

+0

+1 để chỉ ra rằng chúng ta có thể tránh không chỉ sao chép, nhưng xây dựng của đối tượng thừa và phân bổ dự phòng là tốt;) – LihO

+0

Re case (4), nó phụ thuộc vào trình biên dịch (thông minh như thế nào) và các chi tiết cụ thể của mã. Ví dụ, với mã bê tông bạn hiển thị một trình biên dịch thông minh có thể lưu ý rằng việc khởi tạo 'a' không có tác dụng phụ, và rằng khai báo đó có thể được di chuyển xuống dưới' if'. –

+0

@ Cheersandhth.-Alf: Chính xác, nguyên tắc as-if vẫn đứng vững. Tuy nhiên, trong trường hợp chung (nhà xây dựng ngoài tuyến), điều này sẽ chỉ được khấu trừ với LTO được kích hoạt. –

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