2012-03-06 33 views
70

Những gì tôi hiểu là điều này không nên được thực hiện, nhưng tôi tin rằng tôi đã nhìn thấy ví dụ mà làm điều gì đó như thế này (mã lưu ý là không nhất thiết phải đúng cú pháp nhưng ý tưởng là có)Có an toàn khi trả lại cấu trúc trong C hoặc C++ không?

typedef struct{ 
    int a,b; 
}mystruct; 

Và sau đó đây là một chức năng

mystruct func(int c, int d){ 
    mystruct retval; 
    retval.a = c; 
    retval.b = d; 
    return retval; 
} 

tôi hiểu rằng chúng ta nên luôn luôn trả về một con trỏ đến một cấu trúc malloc'ed nếu chúng tôi muốn làm một cái gì đó như thế này, nhưng tôi tích cực tôi đã nhìn thấy những ví dụ mà làm điều gì đó như thế này. Điều này có đúng không? Cá nhân tôi luôn luôn hoặc là trả về một con trỏ đến một cấu trúc malloc'ed hoặc chỉ cần làm một vượt qua bằng cách tham chiếu đến hàm và sửa đổi các giá trị ở đó. (Bởi vì sự hiểu biết của tôi là khi phạm vi chức năng kết thúc, bất kỳ chồng nào được sử dụng để phân bổ cấu trúc có thể được ghi đè).

Hãy thêm phần thứ hai vào câu hỏi: Điều này có khác nhau tùy theo trình biên dịch không? Nếu có, thì hành vi của các phiên bản trình biên dịch mới nhất cho máy tính để bàn là gì: gcc, g ++ và Visual Studio?

Suy nghĩ về vấn đề này?

+29

"Những gì tôi hiểu là điều này không nên được thực hiện" nói ai? Tôi luôn làm việc đó. Cũng lưu ý rằng typedef không cần thiết trong C++ và không tồn tại những thứ như "C/C++". – PlasmaHH

+4

Câu hỏi dường như ** không ** được nhắm mục tiêu tại C++. –

+3

@PlasmaHH Sao chép các cấu trúc lớn xung quanh có thể không hiệu quả. Đó là lý do tại sao người ta phải cẩn thận và suy nghĩ kỹ trước khi trả về một cấu trúc theo giá trị, đặc biệt nếu cấu trúc có một hàm tạo bản sao đắt tiền và trình biên dịch không tốt với tối ưu hóa giá trị trả về. Gần đây tôi đã thực hiện tối ưu hóa cho một ứng dụng đã dành một phần đáng kể thời gian của mình trong các nhà xây dựng bản sao cho một vài cấu trúc lớn mà một lập trình viên đã quyết định quay lại theo giá trị ở mọi nơi. Sự thiếu hiệu quả đã khiến chúng tôi tốn khoảng 800.000 đô la trong phần cứng trung tâm dữ liệu bổ sung mà chúng tôi cần mua. – Crashworks

Trả lời

67

Tính năng này hoàn toàn an toàn và không làm như vậy. Ngoài ra: nó không khác nhau tùy theo trình biên dịch.

Thông thường, khi (như ví dụ của bạn) cấu trúc của bạn không quá lớn, tôi cho rằng cách tiếp cận này thậm chí còn tốt hơn là trả về cấu trúc malloc'ed (malloc là một hoạt động tốn kém).

+2

Nó vẫn sẽ an toàn nếu một trong các trường là một char *? Bây giờ sẽ có con trỏ trong cấu trúc – jzepeda

+0

Có, nó sẽ. Chỉ cần cẩn thận khi bạn malloc/miễn phí mà 'char *'. –

+3

@ user963258 thực sự, điều đó phụ thuộc vào cách bạn triển khai hàm tạo bản sao và hàm hủy. –

61

Nó hoàn toàn an toàn.

Bạn đang quay trở lại theo giá trị. Điều gì sẽ dẫn đến hành vi không xác định là nếu bạn quay trở lại bằng cách tham chiếu.

//safe 
mystruct func(int c, int d){ 
    mystruct retval; 
    retval.a = c; 
    retval.b = d; 
    return retval; 
} 

//undefined behavior 
mystruct& func(int c, int d){ 
    mystruct retval; 
    retval.a = c; 
    retval.b = d; 
    return retval; 
} 

Hành vi của đoạn mã của bạn hoàn toàn hợp lệ và được xác định. Nó không thay đổi tùy theo trình biên dịch. Không sao đâu!

Cá nhân tôi luôn luôn hoặc là trả về một con trỏ đến một cấu trúc malloc'ed

Bạn không nên. Bạn nên tránh bộ nhớ được cấp phát động khi có thể.

hoặc chỉ thực hiện chuyển qua tham chiếu đến hàm và sửa đổi các giá trị tại đó.

Tùy chọn này hoàn toàn hợp lệ. Đó là vấn đề của sự lựa chọn. Nói chung, bạn làm điều này nếu bạn muốn trả lại một cái gì đó khác từ hàm, trong khi sửa đổi cấu trúc ban đầu.

Bởi vì hiểu biết của tôi là một khi phạm vi của hàm là kết thúc, bất cứ điều gì ngăn xếp được sử dụng để phân bổ cơ cấu có thể ghi đè

này là sai. Ý tôi là, nó đúng, nhưng bạn trả lại một bản sao của cấu trúc bạn tạo bên trong hàm. Về mặt lý thuyết. Trong thực tế, RVO có thể và có thể sẽ xảy ra.Đọc về tối ưu hóa giá trị trả lại. Điều này có nghĩa rằng mặc dù retval xuất hiện để đi ra khỏi phạm vi khi chức năng kết thúc, nó thực sự có thể được xây dựng trong bối cảnh gọi điện thoại, để ngăn chặn các bản sao thêm. Đây là một tối ưu hóa trình biên dịch là miễn phí để thực hiện.

+2

+1 để đề cập đến RVO. Tối ưu hóa quan trọng này thực sự làm cho mẫu này khả thi đối với các đối tượng có các hàm tạo bản sao đắt tiền, như các thùng chứa STL. – Kos

+1

Điều đáng nói đến là mặc dù trình biên dịch là miễn phí để thực hiện tối ưu hóa giá trị trả lại, không có gì đảm bảo nó sẽ xảy ra. Đây không phải là thứ bạn có thể tin cậy, chỉ hy vọng thôi. – Watcom

+0

Tôi nhận được các loại xung đột ... – delive

7

Tuổi thọ của đối tượng mystruct trong chức năng của bạn thực sự kết thúc khi bạn rời khỏi chức năng. Tuy nhiên, bạn đang truyền đối tượng theo giá trị trong câu lệnh return. Điều này có nghĩa là đối tượng được sao chép ra khỏi hàm vào hàm gọi. Đối tượng ban đầu đã biến mất, nhưng bản sao vẫn tồn tại.

4

Nó hoàn toàn hợp pháp, nhưng với cấu trúc lớn có hai yếu tố cần được xem xét: tốc độ và kích thước ngăn xếp.

+0

Nghe về tối ưu hóa giá trị trả về? –

+0

Có, nhưng chúng tôi đang nói chung về việc trả về cấu trúc theo giá trị, và có những trường hợp trình biên dịch không thể thực hiện RVO. – ebutusov

+2

Tôi chỉ nói lo lắng về bản sao phụ sau khi bạn đã thực hiện một số hồ sơ. –

2

Tôi cũng sẽ đồng ý với sftrabbit, Cuộc sống thực sự kết thúc và vùng ngăn xếp sẽ bị xóa nhưng trình biên dịch đủ thông minh để đảm bảo rằng tất cả dữ liệu sẽ được truy xuất trong sổ đăng ký hoặc theo cách khác.

Một ví dụ đơn giản để xác nhận được đưa ra dưới đây. (Lấy từ lắp ráp biên dịch MinGW)

_func: 
    push ebp 
    mov ebp, esp 
    sub esp, 16 
    mov eax, DWORD PTR [ebp+8] 
    mov DWORD PTR [ebp-8], eax 
    mov eax, DWORD PTR [ebp+12] 
    mov DWORD PTR [ebp-4], eax 
    mov eax, DWORD PTR [ebp-8] 
    mov edx, DWORD PTR [ebp-4] 
    leave 
    ret 

Bạn có thể thấy rằng giá trị của b đã được truyền qua edx. trong khi mặc định eax chứa giá trị cho a.

3

Loại cấu trúc có thể là loại cho giá trị được trả về bởi hàm. Nó là an toàn vì trình biên dịch sẽ tạo một bản sao của cấu trúc và trả về bản sao chứ không phải cấu trúc cục bộ trong hàm.

typedef struct{ 
    int a,b; 
}mystruct; 

mystruct func(int c, int d){ 
    mystruct retval; 
    cout << "func:" <<&retval<< endl; 
    retval.a = c; 
    retval.b = d; 
    return retval; 
} 

int main() 
{ 
    cout << "main:" <<&(func(1,2))<< endl; 


    system("pause"); 
} 
3

Hoàn toàn an toàn khi trả lại cấu trúc như bạn đã làm.

Dựa trên tuyên bố này tuy nhiên: Bởi vì sự hiểu biết của tôi là khi phạm vi chức năng kết thúc, bất kỳ chồng nào được sử dụng để phân bổ cấu trúc có thể bị ghi đè, Tôi sẽ chỉ tưởng tượng một trường hợp bất kỳ thành viên nào cấu trúc được phân bổ động (malloc'ed hoặc new'ed), trong trường hợp đó, không có RVO, các thành viên được phân bổ động sẽ bị hủy và bản sao trả về sẽ có một thành viên chỉ vào thùng rác.

+0

Ngăn xếp chỉ được sử dụng tạm thời cho thao tác sao chép. Thông thường, ngăn xếp sẽ được giữ trước cuộc gọi và hàm được gọi sẽ đặt dữ liệu được trả về vào ngăn xếp, sau đó người gọi sẽ lấy dữ liệu này từ ngăn xếp và lưu trữ nó bất cứ nơi nào được gán. Vì vậy, không phải lo lắng ở đó. –

3

Sự an toàn phụ thuộc vào cách cấu trúc chính nó được triển khai. Tôi chỉ vấp vào câu hỏi này trong khi thực hiện một cái gì đó tương tự, và đây là vấn đề tiềm năng.

Trình biên dịch, khi trở về giá trị nào để một vài hoạt động (trong số có thể khác):

  1. Gọi copy constructor mystruct(const mystruct&) (this là một biến tạm thời ngoài chức năng func cấp bởi trình biên dịch riêng của mình)
  2. gọi destructor ~mystruct trên biến được phân bổ bên trong func
  3. gọi mystruct::operator= nếu giá trị trả về là assig ned để cái gì khác với =
  4. gọi destructor ~mystruct vào biến tạm thời được sử dụng bởi trình biên dịch

Bây giờ, nếu mystruct là đơn giản như vậy được mô tả ở đây tất cả là tốt, nhưng nếu nó có con trỏ (như char*) hoặc quản lý bộ nhớ phức tạp hơn, sau đó tất cả phụ thuộc vào cách mystruct::operator=, mystruct(const mystruct&)~mystruct được triển khai. Vì vậy, tôi đề nghị cảnh báo khi trả về cấu trúc dữ liệu phức tạp dưới dạng giá trị.

1

Không an toàn để trả về cấu trúc. Tôi thích tự mình làm điều đó, nhưng nếu ai đó sẽ thêm một hàm tạo bản sao vào cấu trúc trả về sau, thì hàm tạo bản sao sẽ được gọi. Điều này có thể bất ngờ và có thể phá vỡ mã. Lỗi này rất khó tìm.

Tôi có câu trả lời phức tạp hơn, nhưng người kiểm duyệt không thích nó. Vì vậy, tại thời điểm của bạn, tip của tôi là ngắn.

+0

“Không an toàn để trả lại cấu trúc. […] Người tạo bản sao sẽ được gọi. ”- Có sự khác biệt giữa * safe * và * không hiệu quả *. Trả về một cấu trúc chắc chắn là an toàn. Thậm chí sau đó, các cuộc gọi đến các bản sao ctor rất có thể sẽ được elided bởi trình biên dịch như cấu trúc được tạo ra trên stack của người gọi để bắt đầu với. – phg

1

Hãy thêm phần thứ hai vào câu hỏi: Điều này có khác nhau tùy theo trình biên dịch không?

Trên thực tế đúng như vậy, khi tôi phát hiện nỗi đau của tôi: http://sourceforge.net/p/mingw-w64/mailman/message/33176880/

Tôi đã sử dụng gcc trên win32 (MinGW) để gọi giao diện COM đó quay trở lại cấu trúc. Hóa ra rằng MS thực hiện nó khác với GNU và vì vậy chương trình (gcc) của tôi bị hỏng với một ngăn xếp bị hỏng.

Có thể MS có thể có nền tảng cao hơn ở đây - nhưng tất cả những gì tôi quan tâm là khả năng tương thích ABI giữa MS và GNU để xây dựng trên Windows. gcc, g ++ và Visual Studio

Bạn có thể tìm thấy một số tin nhắn trong danh sách Rượu gửi thư về cách MS:

Nếu có, thì hành vi cho các phiên bản mới nhất của trình biên dịch cho máy tính để bàn là những gì dường như làm điều đó.

+0

Sẽ hữu ích hơn nếu bạn đưa một con trỏ đến danh sách gửi thư Wine mà bạn đang đề cập đến. –

+0

Quay trở lại cấu trúc là tốt. COM chỉ định một giao diện nhị phân; nếu ai đó không thực hiện COM đúng thì đó sẽ là một lỗi. –

5

Không chỉ nó là an toàn để trở về một struct trong C (hoặc một class trong C++, nơi struct -s thực sự là class -es với mặc định public: thành viên), nhưng rất nhiều phần mềm được làm điều đó.

Tất nhiên, khi trả về class bằng C++, ngôn ngữ chỉ định rằng một số hàm khởi tạo hoặc hàm khởi động sẽ được gọi, nhưng có nhiều trường hợp điều này có thể được tối ưu hóa bởi trình biên dịch.

Bên cạnh đó, Linux x86-64 ABI xác định rằng việc trả lại một struct với hai vô hướng (ví dụ con trỏ, hoặc long) giá trị được thực hiện thông qua thanh ghi (%rax & %rdx) như vậy là rất nhanh và hiệu quả. Vì vậy, đối với trường hợp cụ thể đó, có lẽ nhanh hơn để trả lại trường hai trường vô hướng struct hơn là làm bất kỳ điều gì khác (ví dụ: lưu trữ chúng vào một con trỏ được chuyển làm đối số).

Trả lại trường hai trường vô hướng struct sau đó nhanh hơn rất nhiều so với malloc -ing và trả về con trỏ.

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