Jaka jest zaleta zwracania wskaźnika do struktury w porównaniu do zwracania całej struktury w return
instrukcji funkcji?
Mówię o funkcjach takich jak fopen
i innych funkcjach niskiego poziomu, ale prawdopodobnie istnieją funkcje wyższego poziomu, które zwracają również wskaźniki do struktur.
Uważam, że jest to raczej wybór projektowy niż tylko kwestia programowania i jestem ciekawy, aby dowiedzieć się więcej o zaletach i wadach tych dwóch metod.
Jednym z powodów, dla których pomyślałem, że byłoby korzystne, aby zwrócić wskaźnik do struktury, jest łatwiejsze określenie, czy funkcja zakończyła się niepowodzeniem przez zwrócenie NULL
wskaźnika.
Zwrócenie pełnej struktury, która NULL
byłaby trudniejsza, mniej wydajna. Czy to ważny powód?
c
data-structures
functions
low-level
return-type
yoyo_fun
źródło
źródło
gets()
funkcji. Niektórzy programiści nadal mają awersję do kopiowania struktur, stare nawyki bardzo umierają.FILE*
jest skutecznie nieprzezroczystym uchwytem. Kod użytkownika nie powinien dbać o jego wewnętrzną strukturę.&
i uzyskać dostęp do członka.
”.Odpowiedzi:
Istnieje kilka praktycznych powodów, dla których funkcje takie jak
fopen
wskaźniki powrotu zamiast instancjistruct
typów:struct
typu przed użytkownikiem;W przypadku typów takich
FILE *
jest to spowodowane tym, że nie chcesz ujawniać użytkownikowi szczegółów reprezentacji typu -FILE *
obiekt służy jako nieprzezroczysty uchwyt, a Ty po prostu przekazujesz ten uchwyt do różnych procedur we / wy (i chociażFILE
jest to często zaimplementowany jakostruct
typ, nie musi tak być).Możesz więc ujawnić gdzieś niekompletny
struct
typ w nagłówku:Chociaż nie możesz zadeklarować wystąpienia niekompletnego typu, możesz zadeklarować do niego wskaźnik. Więc mogę utworzyć
FILE *
i przypisać do niego poprzezfopen
,freopen
itp, ale nie mogą bezpośrednio manipulować obiekt to wskazuje.Jest również prawdopodobne, że
fopen
funkcja przydzielaFILE
obiekt dynamicznie, używającmalloc
lub podobnie. W takim przypadku warto zwrócić wskaźnik.Wreszcie możliwe jest, że przechowujesz jakiś stan w
struct
obiekcie i musisz udostępnić ten stan w kilku różnych miejscach. Jeśli zwrócisz instancje tegostruct
typu, będą one oddzielnymi obiektami w pamięci i ostatecznie zsynchronizują się. Zwracając wskaźnik do jednego obiektu, wszyscy odnoszą się do tego samego obiektu.źródło
Istnieją dwa sposoby „zwrócenia struktury”. Możesz zwrócić kopię danych lub referencję (wskaźnik) do niej. Generalnie preferowane jest zwrócenie (i ogólnie przekazanie) wskaźnika z kilku powodów.
Po pierwsze, kopiowanie struktury zajmuje dużo więcej czasu procesora niż kopiowanie wskaźnika. Jeśli jest to czynność często wykonywana przez kod, może powodować zauważalną różnicę wydajności.
Po drugie, bez względu na to, ile razy kopiujesz wskaźnik, nadal wskazuje on na tę samą strukturę w pamięci. Wszystkie modyfikacje zostaną odzwierciedlone w tej samej strukturze. Ale jeśli skopiujesz samą strukturę, a następnie dokonasz modyfikacji, zmiana pojawi się tylko na tej kopii . Kod zawierający inną kopię nie zobaczy zmiany. Czasami, bardzo rzadko, tego właśnie chcesz, ale przez większość czasu tak nie jest i może powodować błędy, jeśli pomylisz się.
źródło
Oprócz innych odpowiedzi czasem warto zwrócić niewielką
struct
wartość. Na przykład można zwrócić parę jednych danych i związany z nimi kod błędu (lub sukcesu).Na przykład
fopen
zwraca tylko jedno dane (otwarteFILE*
), aw przypadku błędu podaje kod błędu przezerrno
zmienną pseudo-globalną. Ale być może lepiej byłoby zwrócić jedenstruct
z dwóch elementów:FILE*
uchwyt i kod błędu (który zostałby ustawiony, jeśli uchwyt pliku jestNULL
). Z przyczyn historycznych tak nie jest (a błędy zgłaszane są przezerrno
globalny, który dziś jest makrem).Zauważ, że język Go ma niezłą notację zwracającą dwie (lub kilka) wartości.
Zauważ też, że w Linux / x86-64 ABI i konwencje wywoływania (patrz strona x86-psABI ) określają, że jeden
struct
z dwóch elementów skalarnych (np. Wskaźnik i liczba całkowita lub dwa wskaźniki lub dwie liczby całkowite) jest zwracany przez dwa rejestry (a to jest bardzo wydajne i nie przechodzi przez pamięć).Tak więc w nowym kodzie C zwracanie małego C
struct
może być bardziej czytelne, przyjazne dla wątków i bardziej wydajne.źródło
rdx:rax
. Więcstruct foo { int a,b; };
jest zwracany zapakowanyrax
(np. Z shift / lub) i musi być rozpakowany za pomocą shift / mov. Oto przykład na Godbolt . Ale x86 może używać niskich 32 bitów 64-bitowego rejestru do operacji 32-bitowych bez dbania o wysokie bity, więc zawsze jest to złe, ale zdecydowanie gorsze niż używanie 2 rejestrów przez większość czasu dla struktur 2-członowych.std::optional<int>
zwraca wartość logiczną w górnej połowierax
, więc potrzebujesz 64-bitowej stałej maski do jej przetestowaniatest
. Lub możesz użyćbt
. Ale to jest do bani dla dzwoniącego i odbierającego w porównaniu do używaniadl
, które kompilatory powinny zrobić dla „prywatnych” funkcji. Związane również: libstdc ++ 'sstd::optional<T>
nie jest trywialnie-copyable nawet gdy T jest więc zawsze zwraca poprzez ukryty wskaźnik: stackoverflow.com/questions/46544019/... . (libc ++ można w prosty sposób kopiować)struct { int a; _Bool b; };
w C, jeśli rozmówca chciał przetestować logiczną, ponieważ trywialnie-copyable elemencie C ++ używać tego samego ABI jako Cdiv_t div()
Jesteś na dobrej drodze
Oba wymienione przez Ciebie powody są ważne:
Jeśli masz teksturę (na przykład) gdzieś w pamięci i chcesz odwoływać się do tej tekstury w kilku miejscach w programie; nie byłoby rozsądnie tworzyć kopii za każdym razem, gdy chciałbyś się do niej odwoływać. Zamiast tego, jeśli po prostu przekażesz wskaźnik, aby odnieść się do tekstury, twój program będzie działał znacznie szybciej.
Największym powodem jest jednak dynamiczna alokacja pamięci. Często podczas kompilacji programu nie masz pewności, ile dokładnie pamięci potrzebujesz na określone struktury danych. Gdy tak się stanie, ilość pamięci, którą należy użyć, zostanie określona w czasie wykonywania. Możesz zażądać pamięci za pomocą „malloc”, a następnie zwolnić ją, gdy skończysz używać „free”.
Dobrym przykładem tego jest czytanie z pliku określonego przez użytkownika. W takim przypadku nie masz pojęcia, jak duży może być plik podczas kompilacji programu. Możesz tylko dowiedzieć się, ile pamięci potrzebujesz, gdy program faktycznie działa.
Zarówno malloc, jak i bezpłatne wskaźniki powrotu do lokalizacji w pamięci. Funkcje korzystające z dynamicznego przydziału pamięci zwracają wskaźniki do miejsca, w którym utworzyły swoje struktury w pamięci.
Ponadto w komentarzach widzę pytanie, czy możesz zwrócić strukturę z funkcji. Rzeczywiście możesz to zrobić. Następujące powinny działać:
źródło
struct incomplete* foo(void)
. W ten sposób mogę zadeklarować funkcje w nagłówku, ale tylko zdefiniować struktury w pliku C, co pozwala na enkapsulację.Coś w rodzaju
FILE*
kodu nie jest tak naprawdę wskaźnikiem do struktury, jeśli chodzi o kod klienta, ale jest raczej formą nieprzezroczystego identyfikatora powiązanego z jakimś innym bytem, takim jak plik. Kiedy program wywołujefopen
, na ogół nie obchodzi go żadna zawartość zwracanej struktury - wszystko, na czym mu zależy, to to, że inne funkcjefread
zrobią wszystko, co trzeba z tym zrobić.Jeśli standardowa biblioteka przechowuje
FILE*
informacje o np. Bieżącej pozycji odczytu w tym pliku, wywołanie dofread
musiałoby być w stanie zaktualizować te informacje. Mającfread
otrzymywać wskaźnik doFILE
marek takie proste. Gdybyfread
zamiast tego otrzymałFILE
, nie miałby możliwości zaktualizowaniaFILE
obiektu przechowywanego przez dzwoniącego.źródło
Ukrywanie informacji
Najczęstszym z nich jest ukrywanie informacji . C nie ma, powiedzmy, możliwości tworzenia pól
struct
prywatnych, nie mówiąc już o zapewnieniu metod dostępu do nich.Jeśli więc chcesz silnie uniemożliwić programistom wyświetlanie i manipulowanie zawartością pointee, na przykład
FILE
, jedynym sposobem jest zapobieganie narażeniu ich na definicję poprzez traktowanie wskaźnika jako nieprzezroczystego, którego rozmiar pointee i definicje są nieznane światu zewnętrznemu. DefinicjaFILE
będzie wtedy widoczna tylko dla tych, którzy wykonują operacje wymagające jej definicji, na przykładfopen
, podczas gdy tylko deklaracja struktury będzie widoczna dla nagłówka publicznego.Kompatybilność binarna
Ukrywanie definicji struktury może również pomóc w zapewnieniu oddechu w celu zachowania zgodności binarnej w interfejsach API dylib. Pozwala to implementatorom bibliotek zmieniać pola w nieprzezroczystej strukturze bez naruszania binarnej kompatybilności z tymi, którzy korzystają z biblioteki, ponieważ charakter ich kodu musi tylko wiedzieć, co mogą zrobić ze strukturą, a nie jej wielkości lub pól to ma.
Jako przykład mogę dziś uruchomić niektóre starożytne programy zbudowane w czasach Windows 95 (nie zawsze idealnie, ale zaskakująco wiele nadal działa). Istnieje prawdopodobieństwo, że część kodu tych starożytnych plików binarnych używała nieprzezroczystych wskaźników do struktur, których rozmiar i zawartość zmieniły się od czasów Windows 95. Jednak programy nadal działają w nowych wersjach systemu Windows, ponieważ nie były narażone na zawartość tych struktur. Podczas pracy nad biblioteką, w której ważna jest zgodność binarna, to, na co klient nie jest narażony, zwykle może się zmieniać bez naruszania zgodności wstecznej.
Wydajność
Zazwyczaj jest mniej wydajny, zakładając, że ten typ może praktycznie pasować i być alokowany na stosie, chyba że za sceną jest używany znacznie mniej uogólniony alokator pamięci
malloc
, niż , jak już przydzielona pamięć puli alokatora o stałej wielkości zamiast zmiennej. W tym przypadku jest to kompromis w zakresie bezpieczeństwa, który najprawdopodobniej pozwala twórcom bibliotek zachować niezmienniki (gwarancje koncepcyjne)FILE
.Nie jest to tak ważny powód, przynajmniej z punktu widzenia wydajności, aby
fopen
zwrócić wskaźnik, ponieważ jedynym powodem, dla którego zwracaNULL
to niepowodzenie otwarcia pliku. Byłoby to optymalizowanie wyjątkowego scenariusza w zamian za spowolnienie wszystkich ścieżek wykonywania typowych przypadków. W niektórych przypadkach może istnieć uzasadniony powód produktywności, aby uprościć projekty, aby zwracały wskaźniki i pozwalałyNULL
na zwrot w określonych warunkach.W przypadku operacji na plikach narzut jest stosunkowo dość trywialny w porównaniu z samymi operacjami na plikach, a instrukcji i
fclose
tak nie można uniknąć. Więc to nie tak, że możemy zaoszczędzić klientowi kłopotów z uwolnieniem (zamknięciem) zasobu poprzez ujawnienie definicjiFILE
i zwrócenie jej wartościfopen
lub oczekiwanie znacznego wzrostu wydajności, biorąc pod uwagę względny koszt samych operacji na plikach, aby uniknąć przydziału sterty .Hotspoty i poprawki
W innych przypadkach jednak sprofilowałem wiele marnotrawczego kodu C w starszych bazach kodów z punktami dostępowymi
malloc
i niepotrzebnymi obowiązkowymi brakami pamięci podręcznej w wyniku zbyt częstego używania tej praktyki z nieprzejrzystymi wskaźnikami i niepotrzebnego przydzielania zbyt wielu rzeczy na stosie, czasem w duże pętle.Alternatywną praktyką, której używam, jest ujawnianie definicji struktur, nawet jeśli klient nie ma zamiaru ich modyfikować, używając standardu konwencji nazewnictwa, aby poinformować, że nikt inny nie powinien dotykać pól:
Jeśli w przyszłości pojawią się problemy z kompatybilnością binarną, uważam, że wystarczające jest rezerwowanie dodatkowej przestrzeni na przyszłe cele, na przykład:
Ta zarezerwowana przestrzeń jest trochę marnotrawstwem, ale może uratować życie, jeśli w przyszłości okaże się, że musimy dodać więcej danych
Foo
bez niszczenia plików binarnych, które korzystają z naszej biblioteki.Moim zdaniem ukrywanie informacji i zgodność binarna to zazwyczaj jedyny słuszny powód, aby zezwolić na alokację sterty struktur oprócz struktur o zmiennej długości (które zawsze tego wymagałyby, lub przynajmniej byłyby trochę niewygodne w użyciu, gdyby klient musiał przydzielić pamięć na stosie w sposób VLA do alokacji VLS). Nawet duże struktury są często tańsze w zwracaniu według wartości, jeśli oznacza to, że oprogramowanie działa znacznie bardziej z gorącą pamięcią na stosie. I nawet jeśli nie byłyby tańsze, by zwracać wartość po stworzeniu, można po prostu to zrobić:
... aby zainicjować
Foo
ze stosu bez możliwości zbędnej kopii. Lub klient ma nawet swobodę alokacjiFoo
na stercie, jeśli z jakiegoś powodu tego chce.źródło