Proč mnoho funkcí, které vracejí struktury v jazyce C, ve skutečnosti vrací ukazatele na struktury?

Jaká je výhoda vrácení ukazatele na strukturu oproti vrácení celé struktury v return prohlášení o funkci?

Mluvím o funkcích jako fopen a dalších funkcích nižší úrovně, ale pravděpodobně existují i funkce vyšší úrovně, které také vracejí ukazatele do struktur.

Věřím, že se jedná spíše o výběr designu, než jen o otázku programování, a jsem zvědavý, abych věděl více o výhodách a nevýhodách těchto dvou metod.

Jeden z Důvody, proč jsem si myslel, že by bylo výhodou vrátit ukazatel na strukturu, je snadněji zjistit, zda funkce selhala vrácením ukazatele NULL.

Vrátit úplnou strukturu, která je NULL, by bylo podle mého názoru těžší nebo méně efektivní. Je to platný důvod?

Komentáře

  • @ JohnR.Strohm Zkusil jsem to a ve skutečnosti to funguje. Funkce může vrátit strukturu …. Jaký je tedy důvod, proč se tak nestalo?
  • Předstandardizace C neumožňovala kopírování struktur ani jejich předávání hodnotou. Standardní knihovna C má z té doby mnoho pozdržení, která by se tak dnes nepsala, např. trvalo to do C11, než byla odstraněna naprosto chybně navržená gets() funkce. Někteří programátoři stále mají averzi ke kopírování struktur, staré zvyky umírají tvrdě.
  • FILE* je ve skutečnosti neprůhledný popisovač. Uživatelský kód by se neměl starat o to, jaká je jeho vnitřní struktura.
  • Návrat podle odkazu je pouze rozumné výchozí nastavení, pokud máte sběr odpadků.
  • @ JohnR.Strohm “ velmi starší “ ve vašem profilu se zdá, že se vrátil před rokem 1989 😉 – když ANSI C povolil to, co K & RC didn ‚ t: Kopírování struktur v přiřazeních, předávání parametrů a návratových hodnot. Původní kniha K & R ‚ skutečně uvedena výslovně (já ‚ m parafrázuji): “ se strukturou můžete dělat přesně dvě věci, vzít její adresu pomocí & a získat přístup ke členovi pomocí .. “

Odpověď

Tam existuje několik praktických důvodů, proč funkce jako fopen vrací ukazatele na místo instancí typů struct:

  1. Chcete před uživatelem skrýt reprezentaci typu struct;
  2. objekt přidělíte dynamicky;
  3. vy odkazování na jednu instanci objektu pomocí více odkazů;

V případě typů jako FILE * je to proto, že nemáte chci uživateli vystavit podrobnosti reprezentace typu – a FILE * obje ct slouží jako neprůhledný popisovač a tento popisek stačí předat různým I / O rutinám (a zatímco FILE je často implementován jako struct typ, nemusí být).

Takže můžete někde vystavit neúplný struct typ záhlaví:

typedef struct __some_internal_stream_implementation FILE; 

I když nemůžete deklarovat instanci neúplného typu, můžete na ni deklarovat ukazatel. Mohu tedy vytvořit FILE * a přiřadit jej prostřednictvím fopen, freopen atd. , ale nemohu přímo manipulovat s objektem, na který ukazuje.

Je také pravděpodobné, že funkce fopen přiděluje FILE objekt dynamicky pomocí malloc nebo podobného. V takovém případě má smysl vrátit ukazatel.

Nakonec je možné, že v nějakém objektu struct uložíte nějaký stav a musíte tento stav zpřístupnit na několika různých místech. Pokud byste vrátili instance typu struct, byly by tyto instance navzájem samostatnými objekty v paměti a nakonec by se synchronizovaly. Vrácením ukazatele na jeden objekt všichni odkazují na stejný objekt.

Komentáře

  • Zvláštní výhoda použití ukazatele jako neprůhledným typem je, že samotná struktura se může mezi verzemi knihoven měnit a vy ‚ nemusíte volající překompilovat.
  • @Barmar: Stabilita ABI je obrovský velký prodejní bod C a bez neprůhledných ukazatelů by to nebylo tak stabilní.

Odpověď

Existují dva způsoby, jak„ vrátit strukturu. “Můžete vrátit kopii dat nebo na ni vrátit odkaz (ukazatel).Obecně se dává přednost vrátit (a obecně předat) ukazatel, a to z několika důvodů.

Nejprve kopírování struktury zabere mnohem více času CPU než kopírování ukazatele. Pokud je to něco váš kód dělá často, může to způsobit znatelný rozdíl ve výkonu.

Zadruhé, bez ohledu na to, kolikrát zkopírujete ukazatel, stále ukazuje na stejnou strukturu v paměti. Všechny jeho úpravy se projeví ve stejné struktuře. Pokud však zkopírujete samotnou strukturu a poté provedete úpravu, změna se zobrazí pouze na této kopii . Jakýkoli kód, který obsahuje jinou kopii, změnu neuvidí. Někdy, velmi zřídka, to je to, co chcete, ale většinou to tak není a může to způsobit chyby, pokud se pokazíte.

Komentáře

  • Nevýhodou návratu ukazatelem: nyní jste ‚ mohli sledovat vlastnictví tohoto objektu a je možné osvoboď to. Také směr ukazatele může být nákladnější než rychlá kopie. Existuje mnoho proměnných, takže používání ukazatelů není univerzálně lepší.
  • Také ukazatele jsou dnes 64 bitů na většině platforem pro stolní počítače a servery. V mé kariéře jsem ‚ viděl více než několik struktur, které by se vešly do 64 bitů. Takže můžete ‚ t vždy říci, že kopírování ukazatele stojí méně než kopírování struktury.
  • Toto je většinou dobrá odpověď , ale nesouhlasím s částí někdy, velmi zřídka, to je to, co chcete, ale většinou to ‚ není – právě naopak. Vrácení ukazatele umožňuje několik druhů nežádoucích vedlejších účinků a několik druhů ošklivých způsobů, jak špatně vlastnit ukazatel. V případech, kdy čas procesoru není tak důležitý, dávám přednost variantě kopírování, pokud je to možnost, je to mnohem méně náchylné k chybám.
  • Je třeba poznamenat, že to opravdu platí pouze pro externí API. Pro interní funkce každý i okrajově kompetentní kompilátor posledních desetiletí přepíše funkci, která vrací velkou strukturu, aby jako další argument vzal ukazatel a postavil objekt přímo tam. Argumenty neměnných vs proměnlivých byly provedeny dostatečně často, ale myslím, že se všichni můžeme shodnout, že tvrzení, že neměnné datové struktury jsou téměř nikdy to, co chcete, není pravda.
  • Můžete také zmínit kompilační protipožární stěny jako profesionál pro ukazatele. Ve velkých programech s široce sdílenými záhlavími nedokončené typy s funkcemi zabraňují nutnosti znovu kompilovat pokaždé, když se změní podrobnosti implementace. Lepší chování při kompilaci je ve skutečnosti vedlejším účinkem zapouzdření, kterého je dosaženo při oddělení rozhraní a implementace. Vrácení (a předávání, přiřazování) podle hodnoty vyžaduje informace o implementaci.

Odpověď

Kromě dalších odpovědí , někdy se vyplatí vrátit malý struct podle hodnoty. Například by bylo možné vrátit pár jednoho data a související chybový (nebo úspěšný) kód.

Chcete-li si vzít příklad, fopen vrátí pouze jedno údaje (otevřené FILE*) a v případě chyby poskytne kód chyby prostřednictvím errno pseudo-globální proměnná. Bylo by ale snad lepší vrátit struct dva členy: FILE* popis a chybový kód (který by byl nastaven, pokud popisovač souboru je NULL). Z historických důvodů tomu tak není (a chyby jsou hlášeny prostřednictvím errno globálu, kterým je dnes makro).

Všimněte si, že Jazyk Go má pěknou notaci pro vrácení dvou (nebo několika) hodnot.

Všimněte si také, že v systému Linux / x86-64 ABI a konvence volání (viz stránka x86-psABI ) určuje, že struct dvou skalárních členů (např. ukazatele a celého čísla nebo dvou ukazatelů nebo dvou celých čísel) se vrací přes dva registry (a to je velmi efektivní a nejde do paměti).

Takže v novém C kódu může být vrácení malého C struct čitelnější, přátelštější k vláknům a efektivnější.

Komentáře

  • Ve skutečnosti jsou malé struktury zabaleny do rdx:rax. struct foo { int a,b; }; je tedy vrácen zabalený do rax (např. S shift / or) a musí být vybalen pomocí shift / mov. Zde ‚ je příklad hry Godbolt . Ale x86 může použít 32 bitů 64bitového registru pro 32bitové operace, aniž by se staral o vysoké bity, takže je to vždy ‚ příliš špatné, ale rozhodně horší než použití 2 registruje většinu času pro dvoučlenné struktury.
  • Související: bugs.llvm.org/show_bug.cgi? id = 34840 std::optional<int> vrátí logickou hodnotu v horní polovině rax, takže potřebujete 64bitovou masku konstanta k otestování pomocí test. Nebo můžete použít bt. Ale pro volajícího je to na škodu a volané v porovnání s použitím dl, které by kompilátoři měli udělat pro “ private “ funkce. Také související: libstdc ++ ‚ s std::optional<T> není ‚ t triviálně kopírovatelný, i když je T , takže se vždy vrací pomocí skrytého ukazatele: stackoverflow.com/questions/46544019/… . (libc ++ ‚ s je triviálně kopírovatelný)
  • @PeterCordes: vaše související věci jsou C ++, ne C
  • Jejda, správně. Totéž by platilo přesně na struct { int a; _Bool b; }; v jazyce C, pokud by volající chtěl testovat booleovskou hodnotu, protože triviálně kopírovatelné struktury C ++ používají stejné ABI jako C.
  • Klasický příklad div_t div()

Odpověď

Jste na správné cestě

Oba uvedené důvody jsou platné:

Jedním z důvodů, proč jsem myslel jsem si, že by bylo výhodou vrátit ukazatel na strukturu, aby bylo možné snadněji zjistit, zda funkce selhala vrácením ukazatele NULL.

Vrácení FULL struktury, která je NULL, by bylo těžší, předpokládám nebo méně efektivní. Je to platný důvod?

Pokud máte texturu (například) někde v paměti a chcete na ni odkazovat na několika místech ve vašem program; nebylo by moudré vytvořit kopii pokaždé, když jste na ni chtěli odkazovat. Místo toho, pokud jednoduše přejdete kolem ukazatele odkazujícího na texturu, váš program poběží mnohem rychleji.

Největší důvod však je dynamická alokace paměti. Když je program zkompilován, často si nejste jisti, kolik paměti potřebujete pro určité datové struktury. Když k tomu dojde, bude za běhu určeno množství paměti, které potřebujete použít. Vyžádejte si paměť pomocí malloc a poté ji uvolněte, až budete hotovi s free.

Dobrým příkladem je čtení ze souboru, který zadal uživatel. V tomto případě nemáte žádné Představte si, jak velký může být soubor, když kompilujete program. Kolik paměti potřebujete, můžete zjistit pouze tehdy, když je program skutečně spuštěn.

Malloc i volné návratové ukazatele na umístění v paměti. Takže funkce , kteří využívají dynamické přidělování paměti, vrátí ukazatele tam, kde vytvořili své struktury v paměti.

Také v komentářích vidím, že existuje otázka, zda můžete vrátit strukturu z funkce. Opravdu to můžete udělat. Mělo by fungovat následující:

struct s1 { int integer; }; struct s1 f(struct s1 input){ struct s1 returnValue = xinput return returnValue; } int main(void){ struct s1 a = { 42 }; struct s1 b= f(a); return 0; } 

Komentáře

  • Jak je možné zjistit, kolik paměti určitá proměnná bude potřebovat, pokud již máte definovaný typ struktury?
  • @JenniferAnderson C má koncept neúplných typů: název typu může být deklarován, ale ještě není definován, takže ‚ velikost není k dispozici. Nemohu deklarovat proměnné tohoto typu, ale mohu deklarovat ukazatele na tento typ, např. struct incomplete* foo(void). Tímto způsobem mohu deklarovat funkce v záhlaví, ale definovat pouze struktury v souboru C, což umožňuje zapouzdření.
  • @amon Takže takto deklarujeme záhlaví funkcí (prototypy / podpisy), než deklarujeme, jak práce se vlastně dělá v C? A je možné udělat to samé se strukturami a odbory v C
  • @JenniferAnderson deklarujete funkci prototypy (funkce bez těl) v hlavičkových souborech a můžete tyto funkce volat v jiném kódu, aniž by znal tělo funkcí, protože kompilátor potřebuje jen vědět, jak uspořádat argumenty a jak přijmout návratovou hodnotu. Než program propojíte, musíte znát funkci definice (tj. S tělem), ale musíte ji zpracovat pouze jednou. Pokud používáte jednoduchý typ, musí také znát strukturu typu ‚, ale ukazatele mají často stejnou velikost a ‚ záleží na použití prototypu ‚.

Odpovědět

Něco jako FILE* není opravdu ukazatel na strukturu, pokud jde o kód klienta, ale je to spíše forma neprůhledného identifikátoru spojeného s některými jiná entita jako soubor. Když program zavolá fopen, obecně se nestará o žádný obsah vrácené struktury – vše, o co se bude starat, je že ostatní funkce, jako je fread, s tím udělají, co potřebují.

Pokud standardní knihovna uchovává FILE* informace o např. aktuální pozici pro čtení v tomto souboru, volání fread by muselo být možné tyto informace aktualizovat. Díky fread přijetí ukazatele na FILE je to snadné. Pokud by fread místo toho obdržel FILE, neměl by žádný způsob aktualizace FILE objektu držen volajícím.

Odpověď

Skrývání informací

Jaká je výhoda vrácení ukazatele na strukturu oproti vrácení celé struktury v příkazu return funkce?

Nejběžnější je skrývání informací . C nemá, řekněme, schopnost učinit pole struct soukromými, natož aby poskytla metody pro jejich přístup.

Takže pokud chcete energicky zabránit vývojářům v tom, aby mohli vidět a manipulovat s obsahem pointee, jako je FILE, jediným způsobem je zabránit jim v vystavení jeho definici zpracováním ukazatele jako neprůhledné, jehož velikost a definice pointee nejsou vnějšímu světu známy. Definici FILE pak uvidí pouze ti, kdo implementují operace, které vyžadují její definici, jako fopen, zatímco ve veřejné hlavičce bude viditelná pouze deklarace struktury.

Binární kompatibilita

Skrytí definice struktury může také pomoci zajistit dýchací prostor pro zachování binární kompatibility v dylib API. Umožňuje implementátorům knihovny měnit pole v neprůhledné struktuře ure bez porušení binární kompatibility s těmi, kteří používají knihovnu, protože povaha jejich kódu potřebuje pouze vědět, co mohou dělat se strukturou, ne jak velká je nebo jaká pole má.

Jako příklad, mohu skutečně spustit některé staré programy vytvořené během éry Windows 95 dnes (ne vždy dokonale, ale překvapivě mnoho z nich stále funguje). Je pravděpodobné, že některé kódy těchto starověkých binárních souborů používaly neprůhledné ukazatele na struktury, jejichž velikost a obsah se od doby Windows 95 změnily. Programy přesto nadále fungují v nových verzích systému Windows, protože nebyly vystaveny obsahu těchto struktur. Při práci v knihovně, kde je důležitá binární kompatibilita, je obecně možné změnit to, čemu klient není vystaven, aniž by došlo k porušení zpětná kompatibilita.

Účinnost

Vrátit úplnou strukturu, která má hodnotu NULL, by bylo podle mého názoru těžší nebo méně efektivní. Je to platný důvod?

Je obvykle méně efektivní za předpokladu, že se typ prakticky vejde a bude přidělen na zásobník, pokud tam není obvykle mnohem méně generalizovaný alokátor paměti, který se používá v zákulisí než malloc, jako alokátor s pevnou velikostí, a nikoli s proměnnou velikostí, který již sdružuje paměť. V tomto případě je to bezpečnostní kompromis pravděpodobně umožnit vývojářům knihovny udržovat invarianty (koncepční záruky) týkající se FILE.

Není to tak platný důvod, alespoň z hlediska výkonu aby fopen vrátil ukazatel, protože jediným důvodem, proč je „d return NULL, je otevření souboru. To by byla optimalizace výjimečného scénáře výměnou za zpomalení všech cest provádění běžných případů. V některých případech může existovat platný důvod produktivity, aby návrhy byly přímočařejší a umožňovaly jim vracet ukazatele, aby bylo možné vrátit NULL za určité post-podmínky.

U operací se soubory je režie relativně poměrně triviální ve srovnání se samotnými operacemi se soubory a manuální potřebě fclose se stejně nelze vyhnout. Není to tak, že můžeme klientovi ušetřit potíže s uvolněním (zavřením) zdroje tak, že odhalíme definici FILE a vrátíme ji o hodnotu v fopen nebo očekávejte velké zvýšení výkonu vzhledem k relativním nákladům na samotné operace se soubory, abyste se vyhnuli přidělení haldy.

Hotspoty a opravy

V ostatních případech jsem ale profiloval spoustu zbytečného C kódu ve starších kódových základnách s aktivními body v malloc a zbytečné vynechání povinné mezipaměti v důsledku příliš častého používání tohoto postupu s neprůhlednými ukazateli a zbytečného přidělování příliš mnoha věcí na hromadu, někdy ve velkých smyčkách.

Alternativní postup, který místo toho používám, je odhalit definice struktur, i když klient nemá v úmyslu je neoprávněně manipulovat, pomocí standardu pro pojmenování, který sděluje, že nikdo jiný by se neměl dotýkat polí:

struct Foo { /* priv_* indicates that you shouldn"t tamper with these fields! */ int priv_internal_field; int priv_other_one; }; struct Foo foo_create(void); void foo_destroy(struct Foo* foo); void foo_something(struct Foo* foo); 

Pokud se v budoucnu vyskytnou problémy s binární kompatibilitou, zjistil jsem, že je dost dobré, abych si zbytečně rezervoval nějaký prostor navíc pro budoucí účely, například:

struct Foo { /* priv_* indicates that you shouldn"t tamper with these fields! */ int priv_internal_field; int priv_other_one; /* reserved for possible future uses (emergency backup plan). currently just set to null. */ void* priv_reserved; }; 

Ten vyhrazený prostor je trochu plýtvání, ale může zachránit život, pokud v budoucnu zjistíme, že do aniž by došlo k rozbití binárních souborů, které používají naši knihovnu.

Podle mého názoru je skrývání informací a binární kompatibilita obvykle jediným slušným důvodem, proč povolit přidělení haldy pouze struktury kromě struktur s proměnnou délkou (které by to vždy vyžadovaly, nebo alespoň byly trochu trapné použít jinak, pokud by klient musel přidělit paměť na zásobníku ve VLA fash ion přidělit VLS). Dokonce i velké struktury jsou často levnější vrátit se podle hodnoty, pokud to znamená, že software pracuje mnohem více s horkou pamětí na zásobníku. A i kdyby nebylo levnější vrátit se podle hodnoty při vytvoření, dalo by se to jednoduše udělat:

int foo_create(struct Foo* foo); ... /* In the client code: */ struct Foo foo; if (foo_create(&foo)) { foo_something(&foo); foo_destroy(&foo); } 

… inicializovat Foo ze zásobníku bez možnosti nadbytečné kopie. Nebo má klient dokonce svobodu přidělit Foo na haldu, pokud z nějakého důvodu chce.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *