Czy można bezpiecznie zwrócić strukturę w C lub C ++?

85

Rozumiem, że nie należy tego robić, ale wydaje mi się, że widziałem przykłady, które robią coś takiego (kod notatki niekoniecznie jest poprawny składniowo, ale pomysł istnieje)

typedef struct{
    int a,b;
}mystruct;

A oto funkcja

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

Zrozumiałem, że jeśli chcemy zrobić coś takiego, zawsze powinniśmy zwrócić wskaźnik do struktury malloc'owej, ale jestem pewien, że widziałem przykłady, które robią coś takiego. Czy to jest poprawne? Osobiście zawsze albo zwracam wskaźnik do struktury malloc'ed, albo po prostu wykonuję przejście przez odniesienie do funkcji i modyfikuję tam wartości. (Ponieważ rozumiem, że po zakończeniu zakresu funkcji, dowolny stos użyty do przydzielenia struktury może zostać nadpisany).

Dodajmy drugą część do pytania: czy to zależy od kompilatora? Jeśli tak, to jakie jest zachowanie najnowszych wersji kompilatorów dla komputerów stacjonarnych: gcc, g ++ i Visual Studio?

Przemyślenia w tej sprawie?

jzepeda
źródło
34
„Rozumiem tylko, że nie powinno się tego robić” - mówi kto? Robię to cały czas. Zauważ również, że typedef nie jest konieczny w C ++ i że nie istnieje coś takiego jak „C / C ++”.
PlasmaHH
4
Wydaje się, że pytanie nie dotyczy języka c ++.
Kapitan Żyrafa
4
@PlasmaHH Kopiowanie dużych konstrukcji może być nieefektywne. Dlatego należy być ostrożnym i dobrze się zastanowić przed zwróceniem struktury według wartości, zwłaszcza jeśli struktura ma kosztowny konstruktor kopiujący, a kompilator nie jest dobry w optymalizacji wartości zwracanej. Niedawno dokonałem optymalizacji aplikacji, która spędzała znaczną część swojego czasu na konstruktorach kopiujących dla kilku dużych struktur, które jeden programista zdecydował się wszędzie zwrócić według wartości. Nieskuteczność kosztowała nas około 800 000 USD dodatkowego sprzętu do centrum danych, który musieliśmy kupić.
Crashworks
8
@Crashworks: Gratulacje, mam nadzieję, że twój szef dał ci podwyżkę.
PlasmaHH
6
@ Crashworks: z pewnością źle jest zawsze zwracać według wartości bez zastanowienia, ale w sytuacjach, w których jest to naturalne, zazwyczaj nie ma bezpiecznej alternatywy, która nie wymaga również tworzenia kopii, więc zwrot według wartości jest najlepszym rozwiązaniem. nie potrzebuje alokacji sterty. Często nawet nie będzie kopii, użycie dobrego kompilatora elision kopiowania powinno wskoczyć, gdy jest to możliwe, aw C ++ 11 semantyka przenoszenia może wyeliminować jeszcze więcej głębokiego kopiowania. Oba mechanizmy nie będą działać poprawnie, jeśli zrobisz cokolwiek innego niż zwrócenie wartości.
lewej ok.

Odpowiedzi:

78

Jest to całkowicie bezpieczne i nie jest to złe. Ponadto: nie różni się w zależności od kompilatora.

Zwykle, gdy (jak w twoim przykładzie) twoja struktura nie jest zbyt duża, argumentowałbym, że takie podejście jest nawet lepsze niż zwracanie struktury malloc'owej ( mallocjest to kosztowna operacja).

Pablo Santa Cruz
źródło
3
Czy nadal byłoby bezpiecznie, gdyby jedno z pól było char *? Teraz w strukturze znajdowałby się wskaźnik
jzepeda
3
@ user963258 w rzeczywistości zależy to od tego, jak zaimplementujesz konstruktor kopiujący i destruktor.
Luchian Grigore
2
@PabloSantaCruz To trudne pytanie. Gdyby było to pytanie egzaminacyjne, egzaminator mógłby spodziewać się odpowiedzi „nie”, jeśli należałoby wziąć pod uwagę własność.
Kapitan Żyrafa
2
@CaptainGiraffe: prawda. Ponieważ OP nie wyjaśnił tego, a jego / jej przykłady były w zasadzie C , założyłem, że jest to bardziej pytanie C niż C ++ .
Pablo Santa Cruz
2
@Kos Niektóre kompilatory nie mają NRVO? Od jakiego roku? Uwaga: w C ++ 11 nawet jeśli nie ma NRVO, zamiast tego wywoła semantykę przenoszenia.
David
73

Jest całkowicie bezpieczny.

Wracasz według wartości. To, co mogłoby prowadzić do niezdefiniowanego zachowania, to powrót przez odniesienie.

//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;
}

Zachowanie twojego fragmentu kodu jest całkowicie poprawne i zdefiniowane. Nie różni się w zależności od kompilatora. W porządku!

Osobiście zawsze zwracam wskaźnik do struktury malloc

Nie powinieneś. Jeśli to możliwe, należy unikać dynamicznie przydzielanej pamięci.

lub po prostu przeprowadź przez odniesienie do funkcji i zmodyfikuj tam wartości.

Ta opcja jest całkowicie poprawna. To kwestia wyboru. Ogólnie rzecz biorąc, robisz to, jeśli chcesz zwrócić coś innego z funkcji, modyfikując oryginalną strukturę.

Ponieważ rozumiem, że po zakończeniu zakresu funkcji, dowolny stos użyty do przydzielenia struktury może zostać nadpisany

To jest źle. Chodziło mi o to, że to trochę poprawne, ale zwracasz kopię struktury, którą tworzysz wewnątrz funkcji. Teoretycznie . W praktyce RVO może i prawdopodobnie wystąpi. Poczytaj o optymalizacji wartości zwrotu. Oznacza to, że chociaż retvalwydaje się, że po zakończeniu funkcji wykracza poza zakres, w rzeczywistości może być zbudowana w kontekście wywołania, aby zapobiec dodatkowej kopii. Jest to optymalizacja, którą kompilator może zaimplementować bezpłatnie.

Luchian Grigore
źródło
3
+1 za wzmiankę o RVO. Ta ważna optymalizacja w rzeczywistości sprawia, że ​​ten wzorzec jest wykonalny dla obiektów z drogimi konstruktorami kopiującymi, takimi jak kontenery STL.
Kos
1
Warto wspomnieć, że chociaż kompilator może przeprowadzić optymalizację wartości zwracanej, nie ma gwarancji, że tak się stanie. To nie jest coś, na co możesz liczyć, tylko nadzieja.
Watcom,
-1 dla „unikania dynamicznie przydzielanej pamięci, jeśli to możliwe”. Zwykle jest to nowa reguła i często prowadzi do kodu, w którym zwracane są DUŻE ilości danych (i zastanawiają się, dlaczego rzeczy działają wolno), gdy prosty wskaźnik może zaoszczędzić dużo czasu. Prawidłowa reguła to zwracanie struktur lub wskaźników opartych na szybkości , wykorzystanie i klarowność .
Lloyd Sargent
10

Czas życia mystructobiektu w twojej funkcji rzeczywiście kończy się, gdy opuścisz funkcję. Jednak przekazujesz obiekt według wartości w instrukcji return. Oznacza to, że obiekt jest kopiowany z funkcji do funkcji wywołującej. Oryginalny obiekt zniknął, ale kopia nadal żyje.

Joseph Mansfield
źródło
9

Nie tylko bezpieczne jest zwrócenie a structw C (lub a classw C ++, gdzie struct-s to w rzeczywistości class-es z domyślnymi public:składnikami), ale robi to wiele programów.

Oczywiście, zwracając classw C ++, język określa, że ​​zostanie wywołany jakiś destruktor lub ruchomy konstruktor, ale istnieje wiele przypadków, w których kompilator mógłby to zoptymalizować.

Dodatkowo, Linux x86-64 ABI określa, że ​​zwracanie a structz dwoma wartościami skalarnymi (np. Wskaźnikami lub long) odbywa się przez rejestry ( %rax& %rdx), więc jest bardzo szybkie i wydajne. Więc w tym konkretnym przypadku prawdopodobnie szybciej jest zwrócić takie dwu-skalarne pola structniż zrobić cokolwiek innego (np. Zapisać je we wskaźniku przekazanym jako argument).

Zwrócenie takiego pola dwuskalarnego structjest wtedy o wiele szybsze niż malloc-zwrócenie go i zwrócenie wskaźnika.

Basile Starynkevitch
źródło
5

Jest to całkowicie legalne, ale przy dużych strukturach należy wziąć pod uwagę dwa czynniki: szybkość i rozmiar stosu.

ebutusov
źródło
1
Słyszałeś o optymalizacji wartości zwrotu?
Luchian Grigore
Tak, ale mówimy ogólnie o zwracaniu struktury według wartości i są przypadki, w których kompilator nie może wykonać RVO.
ebutusov
3
Powiedziałbym, że martw się tylko o dodatkową kopię po wykonaniu pewnego profilowania.
Luchian Grigore
4

Typ struktury może być typem wartości zwracanej przez funkcję. Jest to bezpieczne, ponieważ kompilator utworzy kopię struktury i zwróci kopię, a nie lokalną strukturę w funkcji.

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");
}
haberdar
źródło
4

Bezpieczeństwo zależy od tego, jak sama konstrukcja została wdrożona. Właśnie natknąłem się na to pytanie, wdrażając coś podobnego, a tutaj jest potencjalny problem.

Kompilator zwracając wartość wykonuje kilka operacji (między innymi):

  1. Wywołuje konstruktor kopiujący mystruct(const mystruct&)( thisjest zmienną tymczasową poza funkcją funcprzydzieloną przez sam kompilator)
  2. wywołuje destruktor ~mystructzmiennej, która została w nim zaalokowanafunc
  3. wywołuje, mystruct::operator=jeśli zwrócona wartość jest przypisana do czegoś innego z=
  4. wywołuje destruktor ~mystructna zmiennej tymczasowej używanej przez kompilator

Jeśli mystructjest tak proste, jak to tutaj opisane, wszystko jest w porządku, ale jeśli ma wskaźnik (jak char*) lub bardziej skomplikowane zarządzanie pamięcią, wszystko zależy od tego mystruct::operator=, jak mystruct(const mystruct&)i ~mystructsą zaimplementowane. Dlatego sugeruję ostrożność podczas zwracania złożonych struktur danych jako wartości.

Filippo
źródło
Prawda tylko przed C ++ 11.
Björn Sundin
4

Zwrócenie struktury tak, jak zrobiłeś, jest całkowicie bezpieczne.

Opierając się jednak na tym stwierdzeniu: ponieważ rozumiem, że po zakończeniu zakresu funkcji dowolny stos użyty do alokacji struktury może zostać nadpisany, wyobrażałbym sobie tylko scenariusz, w którym dowolny z elementów struktury zostałby przydzielony dynamicznie ( malloc'ed lub new'ed), w którym to przypadku bez RVO dynamicznie przydzieleni członkowie zostaną zniszczeni, a zwrócona kopia będzie miała członka wskazującego na śmieci.

maress
źródło
Stos jest używany tylko tymczasowo do operacji kopiowania. Zwykle stos byłby rezerwowany przed wywołaniem, a wywoływana funkcja umieszcza dane, które mają zostać zwrócone, na stosie, a następnie wywołujący pobiera te dane ze stosu i przechowuje je tam, gdzie zostaną przypisane. Więc nie martw się.
Thomas Tempelmann
3

Zgadzam się również ze sftrabbit, Życie rzeczywiście się kończy, a obszar stosu zostaje wyczyszczony, ale kompilator jest wystarczająco inteligentny, aby zapewnić, że wszystkie dane powinny zostać odzyskane w rejestrach lub w inny sposób.

Prosty przykład potwierdzenia jest podany poniżej. (Wzięty z zestawu kompilatora 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

Możesz zobaczyć, że wartość b została przesłana przez edx. podczas gdy domyślny eax zawiera wartość dla.

perilbrain
źródło
2

Zwrot konstrukcji nie jest bezpieczny. Uwielbiam to robić sam, ale jeśli ktoś później doda konstruktor kopiujący do zwracanej struktury, zostanie wywołany konstruktor kopiujący. Może to być nieoczekiwane i może spowodować uszkodzenie kodu. Ten błąd jest bardzo trudny do znalezienia.

Miałem bardziej rozbudowaną odpowiedź, ale moderatorowi się to nie podobało. Więc, twoim kosztem, moja wskazówka jest krótka.

Igor Polk
źródło
„Zwrot konstrukcji nie jest bezpieczny. […] Zostanie wywołany konstruktor kopiujący. ” - Jest różnica między bezpiecznym a nieefektywnym . Zwracanie struktury jest zdecydowanie bezpieczne. Nawet wtedy wywołanie kontrolera kopiującego najprawdopodobniej zostanie pominięte przez kompilator, ponieważ struktura jest tworzona na stosie wywołującego.
fot.
2

Dodajmy drugą część do pytania: czy to zależy od kompilatora?

Rzeczywiście tak, jak odkryłem w moim bólu: http://sourceforge.net/p/mingw-w64/mailman/message/33176880/

Używałem gcc na win32 (MinGW) do wywoływania interfejsów COM, które zwracały structs. Okazuje się, że MS robi to inaczej niż GNU, więc mój program (gcc) rozbił się z rozbiciem stosu.

Możliwe, że MS może mieć tutaj wyższą pozycję - ale wszystko, na czym mi zależy, to kompatybilność ABI między MS i GNU do budowania w systemie Windows.

Jeśli tak, to jakie jest zachowanie najnowszych wersji kompilatorów dla komputerów stacjonarnych: gcc, g ++ i Visual Studio

Możesz znaleźć wiadomości na liście mailingowej Wine o tym, jak wydaje się, że MS to robi.

effbiae
źródło
Byłoby bardziej pomocne, gdybyś wskazał listę mailingową Wine, do której się odnosisz.
Jonathan Leffler
Zwracanie struktur jest w porządku. COM określa interfejs binarny; jeśli ktoś nie zaimplementuje poprawnie COM, będzie to błąd.
MM
1

Uwaga: ta odpowiedź dotyczy tylko języka c ++ 11 i nowszych. Nie ma czegoś takiego jak „C / C ++”, są to różne języki.

Nie, nie ma niebezpieczeństwa zwracania obiektu lokalnego według wartości i jest to zalecane. Uważam jednak, że we wszystkich odpowiedziach brakuje ważnej kwestii. Wielu innych twierdziło, że konstrukcja jest kopiowana lub umieszczana bezpośrednio za pomocą RVO. Jednak nie jest to całkowicie poprawne. Spróbuję dokładnie wyjaśnić, jakie rzeczy mogą się zdarzyć podczas zwracania lokalnego obiektu.

Przenieś semantykę

Od czasu C ++ 11 mieliśmy odniesienia do wartości r, które są odniesieniami do obiektów tymczasowych, z których można bezpiecznie ukraść. Na przykład std :: vector ma konstruktor przenoszenia, a także operator przypisania przenoszenia. Oba mają stałą złożoność i po prostu kopiują wskaźnik do danych przenoszonego wektora. Nie będę tutaj wchodził w szczegóły dotyczące semantyki przenoszenia.

Ponieważ obiekt utworzony lokalnie w funkcji jest tymczasowy i wychodzi poza zakres, gdy funkcja zwraca, zwracany obiekt nigdy nie jest kopiowany w c ++ 11 dalej. Konstruktor przenoszenia jest wywoływany na zwracanym obiekcie (lub nie, wyjaśniono później). Oznacza to, że gdybyś miał zwrócić obiekt za pomocą drogiego konstruktora kopiującego, ale niedrogiego konstruktora przenoszenia, takiego jak duży wektor, tylko własność danych jest przenoszona z obiektu lokalnego do obiektu zwracanego - co jest tanie.

Zwróć uwagę, że w twoim konkretnym przykładzie nie ma różnicy między kopiowaniem a przenoszeniem obiektu. Domyślne konstruktory przenoszenia i kopiowania Twojej struktury powodują te same operacje; kopiowanie dwóch liczb całkowitych. Jest to jednak co najmniej tak szybkie, jak jakiekolwiek inne rozwiązanie, ponieważ cała struktura mieści się w 64-bitowym rejestrze procesora (popraw mnie, jeśli się mylę, nie znam zbyt wielu rejestrów procesora).

RVO i NRVO

RVO oznacza optymalizację wartości zwrotu i jest jedną z nielicznych optymalizacji wykonywanych przez kompilatory, która może mieć skutki uboczne. Od C ++ 17 wymagane jest RVO. Zwracając nienazwany obiekt, jest on konstruowany bezpośrednio w miejscu, w którym obiekt wywołujący przypisuje zwracaną wartość. Nie jest wywoływany ani konstruktor kopiujący, ani konstruktor przenoszenia. Bez RVO nienazwany obiekt zostałby najpierw skonstruowany lokalnie, następnie przeniesiony skonstruowany w zwróconym adresie, a następnie lokalny nienazwany obiekt zostałby zniszczony.

Przykład, gdzie RVO jest wymagane (c ++ 17) lub prawdopodobne (przed c ++ 17):

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

NRVO oznacza optymalizację nazwanej wartości zwrotnej i jest tym samym, co RVO, z wyjątkiem tego, że jest wykonywane dla nazwanego obiektu lokalnie dla wywoływanej funkcji. Wciąż nie jest to gwarantowane przez standard (c ++ 20), ale wiele kompilatorów nadal to robi. Zauważ, że nawet z nazwanymi obiektami lokalnymi, w najgorszym przypadku są one przenoszone po zwróceniu.

Wniosek

Jedynym przypadkiem, w którym powinieneś rozważyć nie zwracanie przez wartość, jest nazwany, bardzo duży (jak w rozmiarze stosu) obiekt. Dzieje się tak, ponieważ NRVO nie jest jeszcze gwarantowane (od c ++ 20), a nawet przesuwanie obiektu byłoby powolne. Moim zaleceniem i zaleceniem w Cpp Core Guidelines jest, aby zawsze preferować zwracanie obiektów według wartości (jeśli zwraca wiele wartości, użyj struktury struct (lub krotki)), gdzie jedynym wyjątkiem jest sytuacja, w której obiekt jest drogi do przeniesienia. W takim przypadku użyj parametru odniesienia innego niż const.

NIGDY nie jest dobrym pomysłem zwracanie zasobu, który musi zostać ręcznie zwolniony z funkcji w języku c ++. Nigdy tego nie rób. Przynajmniej użyj std :: unique_ptr lub stwórz własną nielokalną lub lokalną strukturę z destruktorem, który zwalnia swój zasób ( RAII ) i zwraca jego instancję. W takim przypadku dobrym pomysłem byłoby zdefiniowanie konstruktora przenoszenia i operatora przypisania przenoszenia, jeśli zasób nie ma własnej semantyki przenoszenia (i usuwa konstruktor kopiujący / przypisanie).

Björn Sundin
źródło
ciekawe fakty. Myślę, że golang ma coś podobnego.
Mox