Có an toàn để trả về một cấu trúc trong C hoặc C ++ không?


82

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 đã thấy các ví dụ làm điều gì đó như thế này (mã ghi chú không nhất thiết phải chính xác về mặt cú pháp nhưng ý tưởng là có)

typedef struct{
    int a,b;
}mystruct;

Và đâ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 trả về một con trỏ đến một cấu trúc không đúng nếu chúng ta muốn làm điều gì đó như thế này, nhưng tôi khẳng định rằng tôi đã thấy các ví dụ làm điều gì đó như thế này. Điều này có chính xác? Cá nhân tôi luôn trả về một con trỏ đến một cấu trúc sai hoặc chỉ thực hiện chuyển qua 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à một khi phạm vi của hàm kết thúc, bất kỳ ngăn xếp nào được sử dụng để cấp phát cấu trúc đều có thể bị ghi đè).

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

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


33
"Điều tôi hiểu là điều này không nên được thực hiện" nói ai? Tôi đang làm điều đó mọi lúc. Cũng lưu ý rằng typedef không cần thiết trong C ++ và không tồn tại thứ gọi là "C / C ++".
PlasmaHH

4
Câu hỏi dường như không nhắm vào c ++.
Captain Giraffe,

4
@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 nên 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 giỏi trong việc tối ưu hóa giá trị trả về. Gần đây, tôi đã thực hiện tối ưu hóa một ứng dụng đang dành một khoảng thời gian đáng kể trong các trình tạo 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 trả về theo giá trị ở mọi nơi. Sự kém hiệu quả đã khiến chúng tôi tiêu tốn khoảng 800.000 đô la cho phần cứng trung tâm dữ liệu bổ sung mà chúng tôi cần mua.
Crashworks

8
@Crashworks: Xin chúc mừng, tôi hy vọng sếp của bạn đã tăng lương cho bạn.
PlasmaHH

6
@Crashworks: chắc chắn rằng thật tệ khi luôn trả về theo giá trị mà không cần suy nghĩ, nhưng trong những tình huống mà đó là điều tự nhiên, thường không có giải pháp thay thế an toàn nào mà cũng không yêu cầu tạo bản sao, vì vậy trả về theo giá trị là giải pháp tốt nhất. không cần bất kỳ phân bổ heap nào. Thường thì thậm chí sẽ không một bản sao, sử dụng một trình biên dịch sao chép tốt nên nhảy vào khi có thể và trong C ++ 11, ngữ nghĩa di chuyển có thể loại bỏ nhiều hơn việc sao chép sâu. Cả hai cơ chế sẽ không hoạt động bình thường nếu bạn làm bất cứ điều gì khác ngoài việc trả về theo giá trị.
bùng binh bên trái

Câu trả lời:


75

Nó hoàn toàn an toàn và không sai khi làm như vậy. Ngoài ra: nó không thay đổi 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 sẽ lập luận rằng cách tiếp cận này thậm chí còn tốt hơn so với việc trả về một cấu trúc sai lệch ( malloclà một phép toán đắt tiền).


3
Sẽ vẫn an toàn nếu một trong các trường là ký tự *? Bây giờ sẽ có con trỏ trong struct
jzepeda

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

2
@PabloSantaCruz Đó là một câu hỏi khó. Nếu đó là một câu hỏi kiểm tra, giám khảo có thể mong đợi một câu trả lời là "không", nếu quyền sở hữu cần được xem xét.
Captain Giraffe,

2
@CaptainGiraffe: đúng. Vì OP không làm rõ điều này và các ví dụ của anh ấy / cô ấy về cơ bản là C , tôi cho rằng đó là một câu hỏi C nhiều hơn là một câu hỏi C ++ .
Pablo Santa Cruz

2
@Kos Một số trình biên dịch không có NRVO? Từ năm nào? Cũng cần lưu ý: Trong C ++ 11, ngay cả khi nó không có NRVO, nó sẽ gọi ra ngữ nghĩa chuyển động để thay thế.
David

70

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

Bạn đang 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 lại bằng cách tham khảo.

//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 theo trình biên dịch. Được rồi!

Cá nhân tôi luôn trả về một con trỏ đến một cấu trúc không hợp lệ

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ỉ chuyển bằng tham chiếu đến hàm và sửa đổi các giá trị ở đó.

Tùy chọn này hoàn toàn hợp lệ. Đó là một 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ì sự hiểu biết của tôi là khi phạm vi của hàm kết thúc, bất kỳ ngăn xếp nào được sử dụng để cấp phát cấu trúc đều có thể bị ghi đè

Cái này sai. Ý tôi là, nó đúng, nhưng bạn trả về 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 là mặc dù retvaldường như vượt ra khỏi phạm vi khi hàm kết thúc, nhưng nó thực sự có thể được xây dựng trong ngữ cảnh gọi, để ngăn chặn bản sao thừa. Đây là một tối ưu hóa mà trình biên dịch có thể thực hiện miễn phí.


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

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

-1 cho "tránh bộ nhớ được cấp phát động khi có thể." Điều này có xu hướng là một quy tắc mới và thường dẫn đến mã trong đó lượng dữ liệu LỚN được trả về (và chúng giải đố lý do tại sao mọi thứ chạy chậm) khi một con trỏ đơn giản có thể tiết kiệm rất nhiều thời gian. Quy tắc đúng là trả về cấu trúc hoặc con trỏ dựa trên tốc độ , cách sử dụng và sự rõ ràng .
Lloyd Sargent

9

Thời gian tồn tại của mystructđối tượng trong hàm của bạn thực sự kết thúc khi bạn rời khỏi hàm. Tuy nhiên, bạn đang chuyển đối tượng theo giá trị trong câu lệnh trả về. Đ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 gốc đã biến mất, nhưng bản sao vẫn tồn tại.


8

Không chỉ an toàn khi trả về a structtrong C (hoặc a classtrong C ++, trong đó struct-s thực sự là class-es với public:các thành viên mặc định ), mà rất nhiều phần mềm đang làm điều đó.

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

Ngoài ra, Linux x86-64 ABI chỉ định rằng việc trả về a structvới hailong giá trị vô hướng (ví dụ: con trỏ hoặc ) được thực hiện thông qua các thanh ghi ( %rax& %rdx) vì vậy 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ả về các trường hai vô hướng như vậystruct hơn là làm bất cứ điều gì khác (ví dụ: lưu trữ chúng vào một con trỏ được truyền dưới dạng đối số).

Việc trả về một trường hai vô hướng như vậy structnhanh hơn nhiều so với malloc-ing nó và trả về một con trỏ.


5

Nó hoàn toàn hợp pháp, nhưng với các 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.


1
Nghe nói về tối ưu hóa giá trị trả lại?
Luchian Grigore

Có, nhưng chúng ta đ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

3
Tôi muốn nói rằng chỉ lo lắng về bản sao bổ sung sau khi bạn đã hoàn thành một số hồ sơ.
Luchian Grigore

4

Kiểu cấu trúc có thể là kiểu cho giá trị được trả về bởi một hàm. Nó an toàn vì trình biên dịch sẽ tạo một bản sao của struct và trả về bản sao không phải là 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");
}

4

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

Trình biên dịch, khi trả về giá trị thực hiện một vài thao tác (trong số những thao tác có thể khác):

  1. Gọi hàm tạo bản sao mystruct(const mystruct&)( thislà một biến tạm thời bên ngoài hàm funcdo chính trình biên dịch cấp phát)
  2. gọi hàm hủy ~mystruct trên biến đã được cấp phát bên trongfunc
  3. gọi mystruct::operator=nếu giá trị trả về được chỉ định cho một thứ khác với=
  4. gọi hàm hủy ~mystructtrên biến tạm thời được trình biên dịch sử dụng

Bây giờ, nếu mystructlà đơ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 thực hiện. Do đó, tôi đề xuất các lưu ý khi trả về cấu trúc dữ liệu phức tạp dưới dạng giá trị.


Chỉ đúng trước c ++ 11.
Björn Sundin

4

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

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


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

3

Tôi cũng sẽ đồng ý với sftrabbit, Cuộc sống thực sự kết thúc và khu vực ngăn xếp được 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 thanh ghi hoặc theo cách nào đó khác.

Dưới đây là một ví dụ đơn giản để xác nhận. (Lấy từ trình 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 eax mặc định chứa giá trị cho a.


2

Nó không an toàn để trả về một cấu trúc. Tôi thích tự mình làm điều đó, nhưng nếu sau này ai đó sẽ thêm một hàm tạo bản sao vào cấu trúc được trả về, thì hàm tạo bản sao sẽ được gọi. Điều này có thể không mong muốn và có thể phá vỡ mã. Con bọ 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 điều hành không thích nó. Vì vậy, với sự trình bày của bạn, mẹo của tôi rất ngắn.


“Nó không an toàn để trả lại một cấu trúc. […] Hàm tạo bản sao sẽ được gọi. ” - Có sự khác biệt giữa an toànkhông hiệu quả . Trả lại một cấu trúc chắc chắn là an toàn. Ngay cả khi đó, lệnh gọi tới ctor bản sao rất có thể sẽ được trình biên dịch giải thích vì cấu trúc được tạo trên ngăn xếp của người gọi để bắt đầu.
phg

2

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

Quả thực là như vậy, như tôi đã khám phá ra nỗi đau của mình: http://sourceforge.net/p/mingw-w64/mailman/message/33176880/

Tôi đang sử dụng gcc trên win32 (MinGW) để gọi các giao diện COM trả về cấu trúc. Hóa ra MS làm điều đó khác với GNU và do đó chương trình (gcc) của tôi bị lỗi với một ngăn xếp bị phá hủy.

Có thể MS có điểm 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.

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 dành cho máy tính để bàn là gì: gcc, g ++ và Visual Studio

Bạn có thể tìm thấy một số thông báo trong danh sách gửi thư Wine về cách MS có vẻ làm điều đó.


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

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 triển khai COM đúng cách thì đó sẽ là một lỗi.
MM

1

Lưu ý: câu trả lời này chỉ áp dụng cho c ++ 11 trở đi. Không có cái gọi là "C / C ++", chúng là các ngôn ngữ khác nhau.

Không, không có gì nguy hiểm khi trả về một đối tượng cục bộ theo giá trị, và bạn nên làm như vậy. Tuy nhiên, tôi nghĩ rằng có một điểm quan trọng bị thiếu trong tất cả các câu trả lời ở đây. Nhiều người khác đã nói rằng cấu trúc đang được sao chép hoặc được đặt trực tiếp bằng RVO. Tuy nhiên, điều này không hoàn toàn chính xác. Tôi sẽ cố gắng giải thích chính xác những điều nào có thể xảy ra khi trả về một đối tượng cục bộ.

Di chuyển ngữ nghĩa

Kể từ c ++ 11, chúng ta đã có các tham chiếu rvalue là các tham chiếu đến các đối tượng tạm thời có thể bị đánh cắp một cách an toàn. Ví dụ, std :: vector có một hàm tạo di chuyển cũng như một toán tử gán di chuyển. Cả hai đều có độ phức tạp không đổi và chỉ cần sao chép con trỏ đến dữ liệu của vectơ được di chuyển từ đó. Tôi sẽ không đi vào chi tiết hơn về ngữ nghĩa chuyển động ở đây.

Vì một đối tượng được tạo cục bộ trong một hàm là tạm thời và vượt ra ngoài phạm vi khi hàm trả về, một đối tượng được trả về sẽ không bao giờ được sao chép với c ++ 11 trở đi. Hàm khởi tạo di chuyển đang được gọi trên đối tượng được trả về (hoặc không, sẽ giải thích sau). Điều này có nghĩa là nếu bạn trả về một đối tượng bằng một hàm tạo bản sao đắt tiền nhưng hàm tạo di chuyển rẻ tiền, chẳng hạn như một vectơ lớn, thì chỉ có quyền sở hữu dữ liệu được chuyển từ đối tượng cục bộ sang đối tượng được trả về - điều này rẻ.

Lưu ý rằng trong ví dụ cụ thể của bạn, không có sự khác biệt giữa sao chép và di chuyển đối tượng. Các hàm tạo di chuyển và sao chép mặc định của cấu trúc của bạn dẫn đến các hoạt động giống nhau; sao chép hai số nguyên. Tuy nhiên, điều này ít nhất là nhanh hơn bất kỳ giải pháp nào khác vì toàn bộ cấu trúc nằm trong một thanh ghi CPU 64 bit (hãy sửa cho tôi nếu tôi sai, tôi không biết nhiều thanh ghi CPU).

RVO và NRVO

RVO có nghĩa là Tối ưu hóa Giá trị Trả lại và là một trong số rất ít các tối ưu hóa mà trình biên dịch thực hiện có thể có tác dụng phụ. Kể từ c ++ 17, RVO là bắt buộc. Khi trả về một đối tượng không tên, nó được xây dựng trực tiếp tại chỗ mà người gọi chỉ định giá trị trả về. Cả hàm tạo bản sao và hàm tạo di chuyển đều không được gọi. Nếu không có RVO, đối tượng không tên sẽ được xây dựng cục bộ đầu tiên, sau đó di chuyển được xây dựng trong địa chỉ trả về, sau đó đối tượng không tên cục bộ sẽ bị hủy.

Ví dụ trong đó RVO là bắt buộc (c ++ 17) hoặc có thể (trước c ++ 17):

auto function(int a, int b) -> MyStruct {
    // ...
    return MyStruct{a, b};
}

NRVO có nghĩa là Tối ưu hóa Giá trị Trả lại Được đặt tên và giống như RVO ngoại trừ nó được thực hiện cho một đối tượng được đặt tên cục bộ cho hàm được gọi. Điều này vẫn không được đảm bảo bởi tiêu chuẩn (c ++ 20) nhưng nhiều trình biên dịch vẫn làm điều đó. Lưu ý rằng ngay cả với các đối tượng cục bộ được đặt tên, chúng vẫn bị di chuyển tồi tệ nhất khi được trả lại.

Phần kết luận

Trường hợp duy nhất mà bạn nên xem xét không trả về theo giá trị là khi bạn có một đối tượng được đặt tên, rất lớn (như trong kích thước ngăn xếp của nó). Điều này là do NRVO chưa được đảm bảo (kể từ c ++ 20) và thậm chí việc di chuyển đối tượng sẽ rất chậm. Đề xuất của tôi và đề xuất trong Nguyên tắc cốt lõi của Cpp là luôn ưu tiên các đối tượng trả về theo giá trị (nếu nhiều giá trị trả về, hãy sử dụng struct (hoặc tuple)), trong đó ngoại lệ duy nhất là khi đối tượng di chuyển đắt tiền. Trong trường hợp đó, hãy sử dụng tham số tham chiếu không phải const.

KHÔNG BAO GIỜ là một ý kiến ​​hay khi trả về một tài nguyên phải được giải phóng thủ công từ một hàm trong c ++. Không bao giờ làm điều đó. Ít nhất hãy sử dụng std :: unique_ptr hoặc tạo cấu trúc không cục bộ hoặc cục bộ của riêng bạn bằng một trình hủy giải phóng tài nguyên của nó ( RAII ) và trả về một bản sao của nó. Sau đó, cũng sẽ là một ý tưởng hay để xác định hàm tạo di chuyển và toán tử gán di chuyển nếu tài nguyên không có ngữ nghĩa di chuyển của riêng nó (và xóa phương thức tạo / gán bản sao).


sự thật thú vị Tôi nghĩ rằng golang có một cái gì đó tương tự.
Mox
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.