Czy są jakieś wady przekazywania struktur przez wartość w C, zamiast przekazywania wskaźnika?
Jeśli struktura jest duża, istnieje oczywiście aspekt wydajności kopiowania dużej ilości danych, ale w przypadku mniejszej struktury powinno to być to samo, co przekazywanie kilku wartości do funkcji.
Może być nawet bardziej interesujący, gdy jest używany jako wartości zwracane. C ma tylko pojedyncze wartości zwracane przez funkcje, ale często potrzebujesz kilku. Zatem prostym rozwiązaniem jest umieszczenie ich w strukturze i zwrócenie jej.
Czy są jakieś powody za lub przeciw?
Ponieważ może nie być oczywiste dla wszystkich, o czym mówię, podam prosty przykład.
Jeśli programujesz w C, prędzej czy później zaczniesz pisać funkcje, które wyglądają tak:
void examine_data(const char *ptr, size_t len)
{
...
}
char *p = ...;
size_t l = ...;
examine_data(p, l);
To nie jest problem. Jedynym problemem jest to, że musisz uzgodnić ze swoim współpracownikiem, w jakiej kolejności powinny być parametry, więc używasz tej samej konwencji we wszystkich funkcjach.
Ale co się dzieje, gdy chcesz zwrócić te same informacje? Zwykle dostajesz coś takiego:
char *get_data(size_t *len);
{
...
*len = ...datalen...;
return ...data...;
}
size_t len;
char *p = get_data(&len);
Działa to dobrze, ale jest znacznie bardziej problematyczne. Wartość zwracana jest wartością zwracaną, z wyjątkiem tego, że w tej implementacji tak nie jest. Z powyższego nie można stwierdzić, że funkcja get_data nie może sprawdzić, na co wskazuje len. I nie ma nic, co sprawia, że kompilator sprawdza, czy wartość jest faktycznie zwracana przez ten wskaźnik. Więc w przyszłym miesiącu, gdy ktoś inny modyfikuje kod bez jego zrozumienia (ponieważ nie przeczytał dokumentacji?), Ulega on zepsuciu i nikt nie zauważa, lub zaczyna się losowo zawieszać.
Zatem rozwiązanie, które proponuję, to prosta struktura
struct blob { char *ptr; size_t len; }
Przykłady można przepisać w następujący sposób:
void examine_data(const struct blob data)
{
... use data.tr and data.len ...
}
struct blob = { .ptr = ..., .len = ... };
examine_data(blob);
struct blob get_data(void);
{
...
return (struct blob){ .ptr = ...data..., .len = ...len... };
}
struct blob data = get_data();
Z jakiegoś powodu myślę, że większość ludzi instynktownie sprawiłaby, że exam_data wziąłby wskaźnik do struct blob, ale nie rozumiem dlaczego. Wciąż otrzymuje wskaźnik i liczbę całkowitą, jest po prostu znacznie wyraźniejsze, że idą razem. A w przypadku get_data nie można zepsuć w sposób, który opisałem wcześniej, ponieważ nie ma wartości wejściowej dla długości, a musi być zwrócona długość.
źródło
void examine data(const struct blob)
jest błędne.gettimeofday
), zamiast tego używają wskaźników, a ludzie biorą to jako przykład.Odpowiedzi:
W przypadku małych struktur (np. Point, rect) przekazywanie przez wartość jest całkowicie dopuszczalne. Ale oprócz szybkości jest jeszcze jeden powód, dla którego powinieneś uważać przy przekazywaniu / zwracaniu dużych struktur według wartości: przestrzeń stosu.
Wiele programów w C jest przeznaczonych dla systemów wbudowanych, w których pamięć jest na wagę złota, a rozmiary stosu mogą być mierzone w KB lub nawet w bajtach ... Jeśli przekazujesz lub zwracasz struktury według wartości, kopie tych struktur zostaną umieszczone na stos, potencjalnie powodując sytuację, w której nazwa tej witryny pochodzi od ...
Jeśli widzę aplikację, która wydaje się nadmiernie wykorzystywać stos, struktury przekazywane przez wartość są jedną z rzeczy, których szukam w pierwszej kolejności.
źródło
Jednym z powodów, aby tego nie robić, o którym nie wspomniano, jest to, że może to spowodować problem, w którym liczy się zgodność binarna.
W zależności od używanego kompilatora, struktury mogą być przesyłane przez stos lub rejestry w zależności od opcji / implementacji kompilatora
Zobacz: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html
Jeśli dwóch kompilatorów się nie zgadza, sprawy mogą wybuchnąć. Nie trzeba dodawać, że główne powody, aby tego nie robić, są zilustrowane, to zużycie stosu i przyczyny związane z wydajnością.
źródło
int &bar() { int f; int &j(f); return j;};
Aby naprawdę odpowiedzieć na to pytanie, trzeba zagłębić się w teren montażu:
(Poniższy przykład używa gcc na x86_64. Każdy może dodać inne architektury, takie jak MSVC, ARM itp.)
Weźmy przykładowy program:
Skompiluj go z pełnymi optymalizacjami
Spójrz na montaż:
Oto, co otrzymujemy:
Z wyłączeniem
nopl
padówgive_two_doubles()
ma 27 bajtów, agive_point()
ma 29 bajtów. Z drugiej stronygive_point()
daje o jedną instrukcję mniej niżgive_two_doubles()
Co ciekawe, zauważyliśmy, że kompilator był w stanie zoptymalizować
mov
pod kątem szybszych wariantów SSE2movapd
imovsd
. Co więcej,give_two_doubles()
faktycznie przenosi dane do i z pamięci, co spowalnia.Najwyraźniej wiele z tego może nie mieć zastosowania w środowiskach osadzonych (w których obecnie większość czasu dla języka C jest). Nie jestem mistrzem montażu, więc wszelkie komentarze będą mile widziane!
źródło
const
cały czas) i stwierdziłem, że nie ma dużego spadku wydajności (jeśli nie zysku) w kopiowaniu typu pass-by-value wbrew temu, w co wielu mogłoby wierzyć.Proste rozwiązanie zwróci kod błędu jako wartość zwracaną, a wszystko inne jako parametr w funkcji.
Ten parametr może oczywiście być strukturą, ale nie widzę żadnej szczególnej korzyści z przekazywania tego przez wartość, po prostu wysłał wskaźnik.
Przekazywanie struktury według wartości jest niebezpieczne, musisz być bardzo ostrożny, co przekazujesz, pamiętaj, że w C nie ma konstruktora kopiującego, jeśli jeden z parametrów struktury jest wskaźnikiem, wartość wskaźnika zostanie skopiowana, może to być bardzo zagmatwane i trudne utrzymać.
Aby uzupełnić odpowiedź (pełne uznanie dla Roddy'ego ), użycie stosu jest kolejnym powodem, dla którego nie można przekazywać struktury po wartości, wierzcie mi, że debugowanie przepełnienia stosu to prawdziwe PITA.
Odtwórz ponownie, aby skomentować:
Przekazywanie struktury przez wskaźnik oznacza, że jakaś jednostka ma prawo własności do tego obiektu i ma pełną wiedzę o tym, co i kiedy powinno zostać zwolnione. Przekazywanie struktury przez wartość tworzy ukryte odniesienia do wewnętrznych danych struktury (wskaźniki do innych struktur itp.), Co jest trudne do utrzymania (możliwe, ale dlaczego?).
źródło
Jedną rzeczą, o której ludzie tutaj zapomnieli do tej pory wspomnieć (lub ja to przeoczyłem), jest to, że struktury zwykle mają wypełnienie!
Każdy znak to 1 bajt, każdy krótki to 2 bajty. Jak duża jest struktura? Nie, to nie jest 6 bajtów. Przynajmniej nie w bardziej powszechnie używanych systemach. W większości systemów będzie to 8. Problem polega na tym, że wyrównanie nie jest stałe, jest zależne od systemu, więc ta sama struktura będzie miała różne wyrównanie i różne rozmiary w różnych systemach.
Nie tylko to wypełnienie jeszcze bardziej pochłonie Twój stos, ale także dodaje niepewności związanej z brakiem możliwości przewidzenia wypełnienia z wyprzedzeniem, chyba że wiesz, jak działa system, a następnie spojrzysz na każdą strukturę, którą masz w aplikacji i obliczysz rozmiar dla tego. Przekazanie wskaźnika zajmuje przewidywalną ilość miejsca - nie ma żadnej niepewności. Rozmiar wskaźnika jest znany systemowi, jest zawsze równy, niezależnie od tego, jak wygląda struktura, a rozmiary wskaźnika są zawsze dobierane w taki sposób, że są wyrównane i nie wymagają dopełnienia.
źródło
Myślę, że twoje pytanie całkiem dobrze podsumowało sprawę.
Inną zaletą przekazywania struktur według wartości jest to, że własność pamięci jest jawna. Nie ma się co zastanawiać, czy struktura pochodzi ze sterty i kto jest odpowiedzialny za jej uwolnienie.
źródło
Powiedziałbym, że przekazywanie (niezbyt dużych) struktur przez wartość, zarówno jako parametry, jak i jako wartości zwracane, jest całkowicie uzasadnioną techniką. Trzeba oczywiście uważać, aby struktura była albo typu POD, albo żeby semantyka kopiowania była dobrze określona.
Aktualizacja: Przepraszam, miałem swój limit myślenia w C ++. Pamiętam czas, kiedy w C nie było legalne zwracanie struktury z funkcji, ale prawdopodobnie od tego czasu się to zmieniło. Nadal powiedziałbym, że jest to ważne, o ile wszystkie kompilatory, których oczekujesz, obsługują tę praktykę.
źródło
Oto coś, o czym nikt nie wspomniał:
Składowe a
const struct
sąconst
, ale jeśli ten element jest wskaźnikiem (jakchar *
), staje sięchar *const
raczej tym,const char *
czego naprawdę chcemy. Oczywiście moglibyśmy założyć, żeconst
jest to dokumentacja intencji i że każdy, kto ją narusza, pisze zły kod (którym są), ale to nie wystarczy dla niektórych (zwłaszcza tych, którzy spędzili cztery godziny na tropieniu przyczyny wypadek).Alternatywą może być zrobienie a
struct const_blob { const char *c; size_t l }
i użycie tego, ale to raczej bałagan - pojawia się ten sam problem ze schematem nazewnictwa, który mam zetypedef
wskaźnikami. Dlatego większość ludzi trzyma się tylko dwóch parametrów (lub, co bardziej prawdopodobne w tym przypadku, używa biblioteki ciągów).źródło
struct const_blob
problem z rozwiązaniem polega na tym, że nawet jeśliconst_blob
mają elementy różniące się odblob
tylko „pośrednią-stałą”, typystruct blob*
do astruct const_blob*
będą traktowane jako odrębne na potrzeby ścisłej reguły aliasingu. W związku z tym, jeśli kod rzuca ablob*
na aconst_blob*
, każdy kolejny zapis do struktury bazowej przy użyciu jednego typu po cichu unieważni wszelkie istniejące wskaźniki innego typu, tak że każde użycie wywoła niezdefiniowane zachowanie (które zwykle może być nieszkodliwe, ale może być śmiertelne) .Strona 150 Samouczka składania PC na http://www.drpaulcarter.com/pcasm/ zawiera jasne wyjaśnienie, w jaki sposób C pozwala funkcji na zwrócenie struktury:
Używam następującego kodu C, aby zweryfikować powyższe stwierdzenie:
Użyj „gcc -S”, aby wygenerować zestaw dla tego fragmentu kodu C:
Stos przed wywołaniem create:
Stos zaraz po wywołaniu create:
źródło
Chcę tylko wskazać jedną z zalet przekazywania struktur według wartości jest to, że optymalizujący kompilator może lepiej zoptymalizować kod.
źródło