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?
Odpowiedzi:
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 (
malloc
jest to kosztowna operacja).źródło
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!
Nie powinieneś. Jeśli to możliwe, należy unikać dynamicznie przydzielanej pamię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ę.
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ż
retval
wydaje 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.źródło
Czas życia
mystruct
obiektu 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.źródło
Nie tylko bezpieczne jest zwrócenie a
struct
w C (lub aclass
w C ++, gdziestruct
-s to w rzeczywistościclass
-es z domyślnymipublic:
składnikami), ale robi to wiele programów.Oczywiście, zwracając
class
w 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
struct
z dwoma wartościami skalarnymi (np. Wskaźnikami lublong
) 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 polastruct
niż zrobić cokolwiek innego (np. Zapisać je we wskaźniku przekazanym jako argument).Zwrócenie takiego pola dwuskalarnego
struct
jest wtedy o wiele szybsze niżmalloc
-zwrócenie go i zwrócenie wskaźnika.źródło
Jest to całkowicie legalne, ale przy dużych strukturach należy wziąć pod uwagę dwa czynniki: szybkość i rozmiar stosu.
źródło
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"); }
źródło
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):
mystruct(const mystruct&)
(this
jest zmienną tymczasową poza funkcjąfunc
przydzieloną przez sam kompilator)~mystruct
zmiennej, która została w nim zaalokowanafunc
mystruct::operator=
jeśli zwrócona wartość jest przypisana do czegoś innego z=
~mystruct
na zmiennej tymczasowej używanej przez kompilatorJeśli
mystruct
jest tak proste, jak to tutaj opisane, wszystko jest w porządku, ale jeśli ma wskaźnik (jakchar*
) lub bardziej skomplikowane zarządzanie pamięcią, wszystko zależy od tegomystruct::operator=
, jakmystruct(const mystruct&)
i~mystruct
są zaimplementowane. Dlatego sugeruję ostrożność podczas zwracania złożonych struktur danych jako wartości.źródło
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.
źródło
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.
źródło
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.
źródło
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.
Możesz znaleźć wiadomości na liście mailingowej Wine o tym, jak wydaje się, że MS to robi.
źródło
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).
źródło