Waarom retourneren veel functies die structuren in C retourneren, eigenlijk verwijzingen naar structuren?

Wat is het voordeel van het retourneren van een pointer naar een structuur in tegenstelling tot het retourneren van de hele structuur in de return verklaring van de functie?

Ik heb het over functies zoals fopen en andere functies op laag niveau, maar waarschijnlijk zijn er functies op een hoger niveau die ook verwijzingen naar structuren retourneren.

Ik geloof dat dit meer een ontwerpkeuze is dan alleen een programmeervraag en ik ben benieuwd naar de voor- en nadelen van de twee methoden.

Een van de redenen waarvan ik dacht dat het een voordeel zou zijn om een pointer naar een structuur te retourneren, is om gemakkelijker te kunnen zien of de functie is mislukt door NULL pointer te retourneren.

Het teruggeven van een volledige structuur die NULL is, zou moeilijker zijn, denk ik, of minder efficiënt. Is dit een geldige reden?

Reacties

  • @ JohnR.Strohm Ik heb het geprobeerd en het werkt echt. Een functie kan een struct teruggeven … Dus wat is de reden dat niet wordt gedaan?
  • Pre-standaardisatie C stond niet toe dat structs werden gekopieerd of door waarde werden doorgegeven. De C-standaardbibliotheek heeft veel holdouts uit die tijd die tegenwoordig niet zo zouden worden geschreven, bijv. het duurde tot C11 voordat de volkomen verkeerd ontworpen gets() -functie was verwijderd. Sommige programmeurs hebben nog steeds een afkeer van het kopiëren van structuren, oude gewoonten zijn hardnekkig.
  • FILE* is in feite een ondoorzichtig handvat. Het maakt gebruikerscode niet uit wat de interne structuur is.
  • Retourneren door middel van verwijzing is alleen een redelijke standaard als je garbage collection hebt.
  • @ JohnR.Strohm De ” zeer senior ” in je profiel lijkt terug te gaan tot 1989 😉 – toen ANSI C toestond wat K & RC didn ‘ t: kopieer structuren in toewijzingen, doorgeven van parameters en retourwaarden. K & R ‘ s originele boek inderdaad expliciet vermeld (ik ‘ m parafraseren): ” je kunt precies twee dingen doen met een structuur, neem het adres ervan met & en open een lid met .. ”

Antwoord

Daar zijn verschillende praktische redenen waarom functies zoals fopen verwijzingen retourneren in plaats van instanties van struct typen:

  1. U wilt de weergave van het struct type verbergen voor de gebruiker;
  2. U “wijst een object dynamisch toe;
  3. U” bent verwijzen naar een enkele instantie van een object via meerdere referenties;

In het geval van typen als FILE *, is dat omdat je dat niet doet wil details van de representatie van het type aan de gebruiker tonen – een FILE * obje ct dient als een ondoorzichtige handle, en je geeft die handle gewoon door aan verschillende I / O-routines (en hoewel FILE vaak wordt geïmplementeerd als een struct type, het hoeft niet “t te zijn).

U kunt dus een onvolledig struct type ergens in een koptekst weergeven:

typedef struct __some_internal_stream_implementation FILE; 

Hoewel u een instantie van een onvolledig type niet kunt declareren, kunt u er wel een verwijzing naar declareren. Dus ik kan een FILE * maken en eraan toewijzen via fopen, freopen, enz. , maar ik kan het object waarnaar het verwijst niet rechtstreeks manipuleren.

Het is ook waarschijnlijk dat de functie fopen een object dynamisch, gebruikmakend van malloc of iets dergelijks. In dat geval is het logisch om een aanwijzer terug te sturen.

Ten slotte is het mogelijk dat je een soort staat opslaat in een struct object, en je moet die staat op verschillende plaatsen beschikbaar maken. Als u instanties van het type struct zou retourneren, zouden die instanties afzonderlijke objecten in het geheugen van elkaar zijn en uiteindelijk niet meer synchroon lopen. Door een pointer naar een enkel object te retourneren, verwijst iedereen naar hetzelfde object.

Opmerkingen

  • Een bijzonder voordeel van het gebruik van de aanwijzer als een ondoorzichtig type is dat de structuur zelf kan veranderen tussen bibliotheekversies en je hoeft de ‘ niet opnieuw te compileren.
  • @Barmar: Inderdaad, ABI Stability is het enorme verkoopargument van C, en het zou niet zo stabiel zijn zonder ondoorzichtige aanwijzingen.

Answer

Er zijn twee manieren om” een structuur te retourneren “. U kunt een kopie van de gegevens retourneren, of u kunt er een verwijzing (pointer) naar retourneren.Het heeft over het algemeen de voorkeur om een pointer terug te sturen (en in het algemeen door te geven), om een aantal redenen.

Ten eerste kost het kopiëren van een structuur veel meer CPU-tijd dan het kopiëren van een pointer. Als dit iets is je code vaak doet, kan het een merkbaar prestatieverschil veroorzaken.

Ten tweede, het maakt niet uit hoe vaak je een pointer rond kopieert, het verwijst nog steeds naar dezelfde structuur in het geheugen. Alle wijzigingen eraan zullen worden weerspiegeld in dezelfde structuur. Maar als u de structuur zelf kopieert en vervolgens een wijziging aanbrengt, wordt de wijziging alleen op die kopie weergegeven. Elke code die een andere kopie bevat, zal de verandering niet zien. Soms, zeer zelden, is dit wat je wilt, maar meestal is het dat niet, en het kan bugs veroorzaken als je het fout doet.

Reacties

  • Het nadeel van retourneren per pointer: nu moet je ‘ het eigendom van dat object bijhouden en mogelijk bevrijd het. Ook kan indirecte aanwijzer duurder zijn dan een snelle kopie. Er zijn hier veel variabelen, dus het gebruik van pointers is niet universeel beter.
  • Ook zijn pointers tegenwoordig 64 bits op de meeste desktop- en serverplatforms. Ik ‘ heb in mijn carrière meer dan een paar structuren gezien die in 64 bits zouden passen. Dus je kunt ‘ t altijd zeggen dat het kopiëren van een pointer minder kost dan het kopiëren van een struct.
  • Dit is meestal een goed antwoord , maar ik ben het oneens over het deel soms, zeer zelden, dit is wat je wilt, maar meestal is het ‘ niet – integendeel. Als u een aanwijzer retourneert, zijn er verschillende soorten ongewenste bijwerkingen en verschillende soorten vervelende manieren om het eigendom van een aanwijzer verkeerd te krijgen. In gevallen waar de CPU-tijd niet zo belangrijk is, geef ik de voorkeur aan de kopie-variant, als dat een optie is, is deze veel minder foutgevoelig.
  • Opgemerkt moet worden dat dit echt is alleen van toepassing op externe APIs. Voor interne functies zal elke zelfs marginaal competente compiler van de afgelopen decennia een functie herschrijven die een grote struct retourneert om een pointer als een extra argument te nemen en het object daar direct in construeren. De argumenten van onveranderlijk versus veranderlijk zijn vaak genoeg gedaan, maar ik denk dat we het er allemaal over eens kunnen zijn dat de bewering dat onveranderlijke datastructuren bijna nooit zijn wat je wilt, niet waar is.
  • Je zou ook compilatie vuurmuren kunnen noemen als een pro voor aanwijzingen. In grote programmas met breed gedeelde headers voorkomen onvolledige typen met functies de noodzaak om elke keer dat een implementatiedetail verandert, opnieuw te compileren. Het betere compilatiegedrag is eigenlijk een neveneffect van de inkapseling die wordt bereikt wanneer interface en implementatie worden gescheiden. Het retourneren (en doorgeven, toewijzen) op waarde heeft de implementatie-informatie nodig.

Antwoord

Naast andere antwoorden , soms is het de moeite waard om een kleine struct waarde te retourneren. Men zou bijvoorbeeld een paar van één gegevens kunnen retourneren, en een foutcode (of succescode) die eraan gerelateerd is.

Om een voorbeeld te nemen, fopen geeft alleen één gegevens (de geopende FILE*) en in geval van een fout, geeft de foutcode door de errno pseudo-globale variabele. Maar het zou misschien beter zijn om een struct van twee leden te retourneren: de FILE* handle en de foutcode (die wordt ingesteld als de bestandsingang is NULL). Om historische redenen is dit niet het geval (en fouten worden gerapporteerd via de errno global, die tegenwoordig een macro is).

Merk op dat de Go-taal heeft een mooie notatie om twee (of een paar) waarden te retourneren.

Merk ook op dat onder Linux / x86-64 de ABI en aanroepconventies (zie x86-psABI pagina) geeft aan dat een struct van twee scalaire leden (bijv. een pointer en een geheel getal, of twee aanwijzers, of twee gehele getallen) wordt geretourneerd via twee registers (en dit is zeer efficiënt en gaat niet door het geheugen).

Dus in nieuwe C-code kan het retourneren van een kleine C struct leesbaarder, thread-vriendelijker en efficiënter zijn.

Opmerkingen

  • Eigenlijk worden kleine structuren verpakt in rdx:rax. Dus struct foo { int a,b; }; wordt teruggestuurd verpakt in rax (bijv. Met shift / of), en moet worden uitgepakt met shift / mov. Hier ‘ is een voorbeeld van Godbolt . Maar x86 kan de lage 32 bits van een 64-bits register gebruiken voor 32-bits bewerkingen zonder rekening te houden met de hoge bits, dus het is ‘ altijd jammer, maar zeker erger dan 2 registreert zich meestal voor 2-ledige structs.
  • Gerelateerd: bugs.llvm.org/show_bug.cgi? id = 34840 std::optional<int> retourneert de booleaanse waarde in de bovenste helft van rax, dus je hebt een 64-bits masker nodig constante om het te testen met test. Of u kunt bt gebruiken. Maar het is waardeloos voor de beller en de aangeroepen persoon in vergelijking met het gebruik van dl, wat compilers zouden moeten doen voor ” privé ” functies. Ook gerelateerd: libstdc ++ ‘ s std::optional<T> isn ‘ t triviaal te kopiëren, zelfs als T is , dus het keert altijd terug via een verborgen aanwijzer: stackoverflow.com/questions/46544019/… . (libc ++ ‘ s is triviaal kopieerbaar)
  • @PeterCordes: je gerelateerde zaken zijn C ++, niet C
  • Oeps, juist. Hetzelfde zou exact van toepassing zijn op struct { int a; _Bool b; }; in C, als de beller de boolean wilde testen, omdat triviaal kopieerbare C ++ -structuren dezelfde ABI gebruiken als C.
  • Klassiek voorbeeld div_t div()

Antwoord

Je bent op de goede weg

Beide redenen die je noemde zijn geldig:

Een van de redenen waarom ik dacht dat het een voordeel zou zijn om een pointer naar een structuur te retourneren, is om gemakkelijker te kunnen zien of de functie is mislukt door een NULL-pointer te retourneren.

Het retourneren van een FULL-structuur die NULL is, zou moeilijker zijn, denk ik of minder efficiënt. Is dit een geldige reden?

Als u een textuur (bijvoorbeeld) ergens in het geheugen heeft, en u wilt naar die textuur verwijzen op verschillende plaatsen in uw programma; het zou niet verstandig zijn om elke keer dat je ernaar zou willen verwijzen een kopie te maken. Als je in plaats daarvan gewoon een aanwijzer doorgeeft om naar de textuur te verwijzen, zal je programma veel sneller werken.

De grootste reden echter is dynamische geheugentoewijzing. Wanneer een programma wordt gecompileerd, weet u vaak niet precies hoeveel geheugen u nodig heeft voor bepaalde gegevensstructuren. Wanneer dit gebeurt, wordt de hoeveelheid geheugen die u moet gebruiken tijdens runtime bepaald. vraag geheugen aan met behulp van malloc en maak het vervolgens vrij als u klaar bent met het gebruik van gratis.

Een goed voorbeeld hiervan is het lezen van een bestand dat door de gebruiker is opgegeven. In dit geval heeft u geen idee hoe groot het bestand kan zijn wanneer u het programma compileert. U kunt alleen achterhalen hoeveel geheugen u nodig heeft wanneer het programma daadwerkelijk wordt uitgevoerd.

Zowel malloc- als free return-verwijzingen naar locaties in het geheugen. Dus functies die gebruik maken van dynamische geheugentoewijzing zullen verwijzingen teruggeven naar waar ze hun structuren in het geheugen hebben gemaakt.

Ook zie ik in de opmerkingen dat er een vraag is of je een struct van een functie kunt retourneren. U kunt dit inderdaad doen. Het volgende zou moeten werken:

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

Reacties

  • Hoe is het mogelijk om niet te weten hoeveel geheugen heb je een bepaalde variabele nodig als je het struct-type al hebt gedefinieerd?
  • @JenniferAnderson C heeft een concept van onvolledige typen: een typenaam kan worden gedeclareerd maar nog niet gedefinieerd, dus ‘ s maat is niet beschikbaar. Ik kan geen variabelen van dat type declareren, maar wel pointers naar dat type, bijv. struct incomplete* foo(void). Op die manier kan ik functies in een header declareren, maar alleen de structs binnen een C-bestand definiëren, waardoor inkapseling mogelijk is.
  • @amon Dus dit is hoe functiekoppen (prototypes / handtekeningen) worden gedeclareerd voordat ze aangeven hoe ze werk wordt eigenlijk gedaan in C? En het is mogelijk om hetzelfde te doen met de structuren en vakbonden in C
  • @JenniferAnderson je declareert functie prototypes (functies zonder body) in header-bestanden en kunt dan die functies aanroepen in andere code, zonder de body van de functies te kennen, omdat de compiler alleen hoeft te weten hoe de argumenten moeten worden gerangschikt en hoe de retourwaarde moet worden geaccepteerd. Tegen de tijd dat je het programma koppelt, moet je de functie definition (dus met een body) eigenlijk kennen, maar dat hoef je maar één keer te verwerken. Als u een niet-eenvoudig type gebruikt, moet het ook de structuur van het type ‘ weten, maar aanwijzers hebben vaak dezelfde grootte en ‘ is niet van belang voor een prototype ‘ s gebruik.

Antwoord

Iets als een FILE* is niet echt een verwijzing naar een structuur wat betreft de clientcode, maar is in plaats daarvan een vorm van ondoorzichtige identificatie die is gekoppeld aan een of andere andere entiteit zoals een bestand. Wanneer een programma fopen aanroept, geeft het over het algemeen niets om de inhoud van de geretourneerde structuur – het enige waar het om gaat is dat andere functies zoals fread zullen doen wat ze ermee moeten doen.

Als een standaardbibliotheek binnen een FILE* informatie houdt over bijv. de huidige leespositie in dat bestand, een aanroep naar fread zou die informatie moeten kunnen bijwerken. Door fread een pointer te laten ontvangen naar de FILE, is dat gemakkelijk. Als fread in plaats daarvan een FILE zou ontvangen, zou het object FILE niet kunnen worden bijgewerkt vastgehouden door de beller.

Antwoord

Informatie verbergen

Wat is het voordeel van het retourneren van een pointer naar een structuur in plaats van het retourneren van de hele structuur in de retourinstructie van de functie?

De meest voorkomende is het verbergen van informatie . C heeft bijvoorbeeld niet de mogelijkheid om velden van een struct privé te maken, laat staan methoden te bieden om er toegang toe te krijgen.

Dus als je krachtig wilt te voorkomen dat ontwikkelaars de inhoud van een pointee kunnen zien en ermee kunnen knoeien, zoals FILE, dan is de enige manier om te voorkomen dat ze worden blootgesteld aan de definitie door de aanwijzer te behandelen als ondoorzichtig waarvan de grootte en definitie van de pointe voor de buitenwereld onbekend zijn. De definitie van FILE is dan alleen zichtbaar voor degenen die de bewerkingen uitvoeren waarvoor de definitie ervan vereist is, zoals fopen, terwijl alleen de structuurverklaring zichtbaar zal zijn voor de openbare header.

Binaire compatibiliteit

Het verbergen van de structuurdefinitie kan ook helpen ademruimte te bieden om de binaire compatibiliteit in dylib-APIs te behouden. Het stelt bibliotheekimplementeerders in staat om de velden in de ondoorzichtige structuur te wijzigen zeker zonder de binaire compatibiliteit te verbreken met degenen die de bibliotheek gebruiken, aangezien de aard van hun code alleen hoeft te weten wat ze kunnen doen met de structuur, niet hoe groot het is of welke velden het heeft.

Als een Ik kan bijvoorbeeld een aantal oude programmas uitvoeren die vandaag tijdens het Windows 95-tijdperk zijn gebouwd (niet altijd perfect, maar verrassend genoeg werken er nog steeds veel). De kans is groot dat een deel van de code voor die oude binaire bestanden ondoorzichtige verwijzingen gebruikte naar structuren waarvan de grootte en inhoud zijn veranderd ten opzichte van het Windows 95-tijdperk. Toch blijven de programmas werken in nieuwe versies van Windows, aangezien ze niet werden blootgesteld aan de inhoud van die structuren. Wanneer je aan een bibliotheek werkt waar binaire compatibiliteit belangrijk is, mag datgene waaraan de client niet wordt blootgesteld, over het algemeen veranderen zonder te breken. achterwaartse compatibiliteit.

Efficiëntie

Het teruggeven van een volledige structuur die NULL is, zou moeilijker zijn, denk ik, of minder efficiënt. Is dit een geldige reden?

Het is doorgaans minder efficiënt, ervan uitgaande dat het type praktisch kan passen en op de stapel kan worden toegewezen, tenzij er doorgaans veel minder is gegeneraliseerde geheugentoewijzing die achter de schermen wordt gebruikt dan malloc, zoals een allocator-poolgeheugen met een vaste grootte in plaats van een variabele grootte die al is toegewezen. Het is in dit geval een veiligheidsafweging, de meeste waarschijnlijk, om de bibliotheekontwikkelaars in staat te stellen invarianten (conceptuele garanties) te handhaven met betrekking tot FILE.

Het is niet zon geldige reden, tenminste niet vanuit het oogpunt van prestaties om fopen een pointer te laten retourneren, aangezien de enige reden waarom het “d terug NULL terugkomt, is wanneer een bestand niet kan worden geopend. Dat zou het optimaliseren van een uitzonderlijk scenario zijn in ruil voor het vertragen van alle gangbare uitvoeringspaden. In sommige gevallen kan er een geldige productiviteitsreden zijn om ontwerpen eenvoudiger te maken zodat ze verwijzingen retourneren zodat NULL kan worden geretourneerd onder een bepaalde post-conditie.

Voor bestandsbewerkingen is de overhead relatief tamelijk triviaal vergeleken met de bestandsbewerkingen zelf, en de handmatige behoefte aan fclose kan sowieso niet worden vermeden. Het is dus niet zo dat we de klant het gedoe van het vrijmaken (sluiten) van de bron kunnen besparen door de definitie van FILE bloot te leggen en deze op waarde te retourneren in fopen of verwacht veel van een prestatieverbetering gezien de relatieve kosten van de bestandsbewerkingen zelf om een heap-toewijzing te vermijden.

Hotspots en fixes

Voor andere gevallen heb ik “veel verkwistende C-code geprofileerd in verouderde codebases met hotspots in malloc en onnodige verplichte cache-missers als gevolg van het te vaak gebruiken van deze praktijk met ondoorzichtige pointers en het onnodig toewijzen van teveel dingen op de heap, soms in grote lussen.

Een alternatieve praktijk die ik in plaats daarvan gebruik, is om structuurdefinities bloot te leggen, zelfs als het niet de bedoeling is dat de klant ermee knoeit, door een standaard voor naamgeving te gebruiken om te communiceren dat niemand anders de velden mag aanraken:

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); 

Als er problemen zijn met binaire compatibiliteit in de toekomst, dan heb ik het goed genoeg gevonden om gewoon overtollig wat extra ruimte te reserveren voor toekomstige doeleinden, zoals:

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

Die gereserveerde ruimte is een beetje verkwistend, maar kan levens redden als we in de toekomst ontdekken dat we wat meer gegevens moeten toevoegen aan Foo zonder de binaire bestanden die onze bibliotheek gebruiken te doorbreken.

Naar mijn mening is informatie verbergen en binaire compatibiliteit meestal de enige goede reden om alleen heap-toewijzing van structuren naast structs met variabele lengte (die het altijd nodig zouden hebben, of anders een beetje lastig te gebruiken zijn als de client geheugen op de stapel moest toewijzen in een VLA-fash ion om de VLS toe te wijzen). Zelfs grote structuren zijn vaak goedkoper om te retourneren op waarde als dat betekent dat de software veel meer werkt met het hete geheugen op de stapel. En zelfs als ze “niet goedkoper waren om op waarde terug te geven bij creatie, zou je dit eenvoudig kunnen doen:

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

… om van de stapel zonder de mogelijkheid van een overbodige kopie. Of de cliënt heeft zelfs de vrijheid om Foo aan de stapel toe te wijzen als ze dat willen om wat voor reden dan ook.

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *