Hvorfor returnerer mange funktioner, der returnerer strukturer i C, faktisk markører til strukturer?

Hvad er fordelen ved at returnere en markør til en struktur i modsætning til at returnere hele strukturen i return erklæring om funktionen?

Jeg taler om funktioner som fopen og andre funktioner på lavt niveau, men sandsynligvis er der funktioner på højere niveau, der også returnerer markører til strukturer.

Jeg mener, at dette mere er et designvalg snarere end blot et programmeringsspørgsmål, og jeg er nysgerrig efter at vide mere om fordele og ulemper ved de to metoder.

En af grunde til, at jeg troede, at det ville være en fordel at returnere en markør til en struktur, er at være i stand til lettere at fortælle, hvis funktionen mislykkedes ved at returnere NULL -markøren.

At returnere en fuld struktur, der er NULL, ville jeg antage at være sværere eller mindre effektiv. Er dette en gyldig årsag?

Kommentarer

  • @ JohnR.Strohm Jeg prøvede det, og det virker faktisk. En funktion kan returnere en struktur …. Så hvad er årsagen til, at der ikke gøres?
  • Forstandardisering C tillod ikke, at strukter blev kopieret eller overført til værdi. C-standardbiblioteket har mange holdouts fra den æra, der ikke ville blive skrevet sådan i dag, f.eks. det tog indtil C11, før den fuldstændigt fejldesignede gets() -funktion blev fjernet. Nogle programmører har stadig en modvilje mod at kopiere strukturer, gamle vaner dør hårdt.
  • FILE* er faktisk et uigennemsigtigt håndtag. Brugerkode er ligeglad med, hvad den interne struktur er.
  • Returnering som reference er kun en rimelig standard, når du har affaldsindsamling.
  • @ JohnR.Strohm ” meget senior ” i din profil ser ud til at gå tilbage før 1989 😉 – da ANSI C tillod det K & RC gjorde ikke ‘ t: Kopier strukturer i tildelinger, parameteroverførsel og returværdier. K & R ‘ s originale bog angav faktisk eksplicit (I ‘ m omskrivning): ” du kan gøre nøjagtigt to ting med en struktur, tag adressen med & og få adgang til et medlem med .. ”

Svar

Der er flere praktiske grunde til, at funktioner som fopen peger tilbage til i stedet for forekomster af struct typer:

  1. Du vil skjule repræsentationen af struct -typen for brugeren;
  2. Du tildeler et objekt dynamisk;
  3. Du “re henviser til en enkelt forekomst af et objekt via flere referencer;

I tilfælde af typer som FILE * er det fordi du ikke ønsker at eksponere detaljer om typen “s repræsentation for brugeren – en FILE * obje ct fungerer som et uigennemsigtigt håndtag, og du overfører bare håndtaget til forskellige I / O-rutiner (og mens FILE ofte implementeres som en struct type, det behøver ikke at være at være).

Så du kan udsætte en ufuldstændig struct -type i en overskrift et eller andet sted:

typedef struct __some_internal_stream_implementation FILE; 

Selvom du ikke kan erklære en forekomst af en ufuldstændig type, kan du erklære en markør til den. Så jeg kan oprette en FILE * og tildele den via fopen, freopen osv. , men jeg kan ikke direkte manipulere det objekt, det peger på.

Det er også sandsynligt, at fopen -funktionen tildeler en FILE objekt dynamisk ved hjælp af malloc eller lignende. I så fald giver det mening at returnere en markør.

Endelig er det muligt, at du gemmer en slags tilstand i et struct -objekt, og du skal gøre denne tilstand tilgængelig flere forskellige steder. Hvis du returnerede forekomster af typen struct, ville disse forekomster være separate objekter i hukommelsen fra hinanden og til sidst komme ud af synkronisering. Ved at returnere en markør til et enkelt objekt henviser alle til det samme objekt.

Kommentarer

  • En særlig fordel ved at bruge markøren som en uigennemsigtig type er, at selve strukturen kan skifte mellem biblioteksversioner, og du behøver ikke ‘ til at kompilere opkaldene igen.
  • @Barmar: Faktisk er ABI-stabilitet det enorme salgsargument for C, og det ville ikke være så stabilt uden uigennemsigtige henvisninger.

Svar

Der er to måder at” returnere en struktur. “Du kan returnere en kopi af dataene, eller du kan returnere en reference (markør) til den.Det foretrækkes generelt at returnere (og videregive generelt) en markør af et par grunde.

For det første tager kopiering af en struktur meget mere CPU-tid end at kopiere en markør. Hvis dette er noget din kode ofte, kan det medføre en mærkbar forskel i ydelse.

For det andet, uanset hvor mange gange du kopierer en markør rundt, peger den stadig på den samme struktur i hukommelsen. Alle ændringer til den afspejles i den samme struktur. Men hvis du kopierer selve strukturen og derefter foretager en ændring, vises ændringen kun på den kopi . Enhver kode, der indeholder en anden kopi, kan ikke se ændringen. Nogle gange er det meget sjældent, hvad du vil, men det er oftest ikke tilfældet, og det kan forårsage fejl, hvis du tager fejl.

Kommentarer

  • Ulempen ved at vende tilbage med markør: nu skal du ‘ spore ejerskab af objektet og mulig frigør det. Markørindirigering kan også være dyrere end en hurtig kopi. Der er mange variabler her, så det er ikke universelt bedre at bruge pegepinde.
  • Pegepinde er i disse dage 64 bit på de fleste desktop- og serverplatforme. Jeg ‘ har set mere end et par strukter i min karriere, der ville passe i 64 bits. Så du kan ‘ t altid sige, at kopiering af en markør koster mindre end at kopiere en struktur.
  • Dette er for det meste et godt svar , men jeg er uenig om delen nogle gange, meget sjældent, er dette, hvad du vil have, men det meste af tiden ‘ er ikke – tværtimod. Returnering af en markør tillader flere slags uønskede bivirkninger og flere slags ubehagelige måder at få ejerskabet af en markør forkert. I tilfælde, hvor CPU-tid ikke er så vigtig, foretrækker jeg kopivarianten, hvis det er en mulighed, er det meget mindre udsat for fejl.
  • Det skal bemærkes, at dette virkelig gælder kun for eksterne APIer. For interne funktioner vil hver endda marginalt kompetente kompilator i de sidste årtier omskrive en funktion, der returnerer en stor struktur for at tage en markør som et yderligere argument og konstruere objektet direkte derinde. Argumenterne om uforanderlig vs foranderlig er gjort ofte nok, men jeg tror, at vi alle kan være enige om, at påstanden om, at uforanderlige datastrukturer næsten aldrig er, hvad du vil, ikke er sandt.
  • Du kan også nævne kompilering af ildvægge som en pro for tip. I store programmer med vidt delte overskrifter forhindrer ufuldstændige typer med funktioner nødvendigheden af at kompilere igen, hver gang en implementeringsdetalje ændres. Den bedre kompileringsadfærd er faktisk en bivirkning af indkapslingen, der opnås, når interface og implementering er adskilt. Tilbage (og videregive, tildele) efter værdi har brug for implementeringsoplysninger.

Svar

Ud over andre svar , nogle gange er det umagen værd at returnere en lille struct efter værdi. For eksempel kan man returnere et par af en data, og en fejl (eller succes) kode relateret til det.

For at tage et eksempel returnerer fopen bare en data (den åbnede FILE*) og i tilfælde af fejl giver fejlkoden gennem errno pseudo-global variabel. Men det ville måske være bedre at returnere et struct af to medlemmer: FILE* -håndtaget og fejlkoden (som ville blive indstillet, hvis filhåndtaget er NULL). Af historiske årsager er det ikke tilfældet (og fejl rapporteres gennem errno global, som i dag er en makro).

Bemærk, at Go-sprog har en god notation for at returnere to (eller et par) værdier.

Bemærk også, at ABI og opkaldskonventioner (se x86-psABI side) angiver, at en struct af to skalarelementer (f.eks. en markør og et heltal eller to markører eller to heltal) returneres gennem to registre (og dette er meget effektivt og går ikke igennem hukommelse).

Så i ny C-kode kan returnering af en lille C struct være mere læselig, mere trådvenlig og mere effektiv.

Kommentarer

  • Faktisk er små strukturer pakket i rdx:rax. Så struct foo { int a,b; }; returneres pakket i rax (f.eks. Med shift / eller) og skal pakkes ud med shift / mov. Her er ‘ et eksempel på Godbolt . Men x86 kan bruge de lave 32 bits i et 64-bit register til 32-bit operationer uden at bekymre sig om de høje bits, så det er ‘ altid for dårligt, men bestemt værre end at bruge 2 registrerer det meste af tiden for 2-medlems strukturer.
  • Relateret: bugs.llvm.org/show_bug.cgi? id = 34840 std::optional<int> returnerer boolesk i den øverste halvdel af rax, så du har brug for en 64-bit maske konstant for at teste det med test. Eller du kan bruge bt. Men det stinker for den, der ringer op og sammenligner med at bruge dl, hvilke kompilatorer skal gøre for ” private ” -funktioner. Også relateret: libstdc ++ ‘ s std::optional<T> isn ‘ t trivielt kopierbar, selv når T er , så den vender altid tilbage via skjult markør: stackoverflow.com/questions/46544019/… . (libc ++ ‘ s kan trivielt kopieres)
  • @PeterCordes: dine relaterede ting er C ++, ikke C
  • Ups, right. Den samme ting ville nøjagtigt gælde for struct { int a; _Bool b; }; i C, hvis den, der ringer op, ville teste den boolske, fordi trivielt kopierbare C ++ -strukturer bruger den samme ABI som C.
  • Klassisk eksempel div_t div()

Svar

Du er på rette spor

Begge grunde, du nævnte, er gyldige:

En af grundene til at jeg troede, at det ville være en fordel at returnere en markør til en struktur, er at være i stand til lettere at fortælle, hvis funktionen mislykkedes ved at returnere NULL-markøren.

Returnering af en FULD struktur, der er NULL, ville være sværere antager jeg eller mindre effektiv. Er dette en gyldig årsag?

Hvis du har en struktur (for eksempel) et eller andet sted i hukommelsen, og du vil henvise til den tekstur flere steder i din program; det ville ikke være klogt at lave en kopi hver gang du ønskede at henvise til den. Hvis du i stedet blot går rundt om en markør for at henvise til strukturen, kører dit program meget hurtigere.

Den største årsag dog er dynamisk hukommelsesallokering. Ofte når et program er kompileret, er du ikke helt sikker på, hvor meget hukommelse du har brug for til bestemte datastrukturer. Når dette sker, vil den mængde hukommelse, du skal bruge, blive bestemt ved kørsel. Du kan anmod hukommelse ved hjælp af malloc og frigør den derefter, når du er færdig med at bruge gratis.

Et godt eksempel på dette er at læse fra en fil, der er angivet af brugeren. I dette tilfælde har du ingen idé om, hvor stor filen kan være, når du kompilerer programmet. Du kan kun finde ud af, hvor meget hukommelse du har brug for, når programmet rent faktisk kører.

Både malloc og gratis returpegere til placeringer i hukommelsen. Så fungerer der gør brug af dynamisk hukommelsesallokering, vil vende tilbage til steder, hvor de har oprettet deres strukturer i hukommelsen.

Også i kommentarerne ser jeg, at der er et spørgsmål om, hvorvidt du kan returnere en struktur fra en funktion. Du kan virkelig gøre dette. Følgende skal fungere:

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

Kommentarer

  • Hvordan er det muligt at ikke vide, hvor meget hukommelse en bestemt variabel har brug for, hvis du allerede har defineret strukturtypen?
  • @JenniferAnderson C har et koncept af ufuldstændige typer: et typenavn kan erklæres, men endnu ikke defineret, så det ‘ s størrelse er ikke tilgængelig. Jeg kan ikke erklære variabler af den type, men kan erklære markører til den type, f.eks. struct incomplete* foo(void). På den måde kan jeg erklære funktioner i en overskrift, men kun definere strutterne i en C-fil, hvilket giver mulighed for indkapsling.
  • @amon Så det er sådan, deklarerer funktionsoverskrifter (prototyper / signaturer), før jeg erklærer, hvordan de arbejdes der faktisk i C? Og det er muligt at gøre det samme med strukturer og fagforeninger i C
  • @JenniferAnderson, du erklærer funktion prototyper (funktioner uden kroppe) i headerfiler og kan derefter kalde disse funktioner i anden kode uden at kende kroppens funktioner, fordi compileren bare skal vide, hvordan man arrangerer argumenterne, og hvordan man accepterer returværdien. Når du forbinder programmet, skal du faktisk kende funktionen definition (dvs. med en krop), men du behøver kun at behandle det en gang. Hvis du bruger en ikke-simpel type, skal den også vide, at typen ‘ s struktur, men pegepinde er ofte af samme størrelse, og den ‘ t betyder noget for en prototype ‘ s brug.

Svar

Noget som en FILE* er ikke rigtig en markør til en struktur for så vidt angår klientkode, men er i stedet en form for uigennemsigtig identifikator tilknyttet nogle anden enhed som en fil. Når et program kalder fopen, vil det generelt ikke bekymre sig om noget af indholdet af den returnerede struktur – alt det vil bekymre sig om er at andre funktioner som fread gør hvad de har brug for at gøre med det.

Hvis et standardbibliotek holder inden for en FILE* information om f.eks. den aktuelle læseposition inden for den fil, et opkald til fread skal være i stand til at kunne opdatere disse oplysninger. At have fread modtage en markør til FILE gør det let. Hvis fread i stedet modtog en FILE, ville det ikke være nogen måde at opdatere FILE objektet tilbageholdt af den, der ringer op.

Svar

Oplysninger, der skjules

Hvad er fordelen ved at returnere en markør til en struktur i modsætning til at returnere hele strukturen i returopgørelsen af funktionen?

Den mest almindelige er information gemmer sig . C har ikke sige evnen til at gøre felter af en struct private, endsige give metoder til at få adgang til dem.

Så hvis du vil med magt forhindre udviklere i at kunne se og manipulere med indholdet af en pointee, som FILE, så er den eneste måde at forhindre dem i at blive udsat for sin definition ved at behandle markøren som uigennemsigtig, hvis pointerstørrelse og definition er ukendt for omverdenen. Definitionen af FILE vil derefter kun være synlig for dem, der implementerer de operationer, der kræver dens definition, som fopen, mens kun strukturdeklarationen vil være synlig for den offentlige overskrift.

Binær kompatibilitet

At skjule strukturdefinitionen kan også hjælpe med at give vejrtrækningsrum for at bevare binær kompatibilitet i dylib APIer. Det giver bibliotekets implementatorer mulighed for at ændre felterne i den uigennemsigtige struktur uden at bryde binær kompatibilitet med dem, der bruger biblioteket, da deres kodes natur kun behøver at vide, hvad de kan gøre med strukturen, ikke hvor stor den er, eller hvilke felter den har.

Som en for eksempel kan jeg faktisk køre nogle gamle programmer bygget under Windows 95-æraen i dag (ikke altid perfekt, men overraskende mange fungerer stadig). Chancerne er, at nogle af koden til de gamle binære filer brugte uigennemsigtige henvisninger til strukturer, hvis størrelse og indhold er ændret fra Windows 95-æraen. Alligevel fungerer programmerne fortsat i nye versioner af windows, da de ikke var udsat for indholdet af disse strukturer. Når man arbejder på et bibliotek, hvor binær kompatibilitet er vigtig, må det, som klienten ikke udsættes for, generelt ændres uden at bryde bagudkompatibilitet.

Effektivitet

At returnere en fuld struktur, der er NULL, ville være sværere antager jeg eller mindre effektiv. Er dette en gyldig årsag?

Det er typisk mindre effektiv, forudsat at typen praktisk talt kan passe og tildeles på stakken, medmindre der typisk er meget mindre generaliseret hukommelsestildeling, der bruges bag kulisserne end malloc, som en allokeret hukommelse med fast størrelse snarere end allokering med variabel størrelse, der allerede er tildelt. Det er en sikkerhedsafvejning i dette tilfælde, mest sandsynligvis at lade biblioteksudviklerne opretholde invarianter (konceptuelle garantier) relateret til FILE.

Det er ikke sådan en gyldig grund i det mindste set fra et præstationssynspunkt for at få fopen til at returnere en markør, da den eneste grund til, at det “d return NULL mangler at åbne en fil. Det ville være at optimere et ekstraordinært scenario til gengæld for at bremse alle almindelige sager til udførelse af sager. Der kan i nogle tilfælde være en gyldig produktivitetsårsag til at gøre design mere ligetil for at få dem til at vende tilbage, så NULL kan returneres i en eller anden posttilstand.

Til filhandlinger er omkostningerne relativt ret trivielle sammenlignet med filhandlingerne selv, og det manuelle behov for fclose kan alligevel ikke undgås. Så det er ikke som om vi kan spare klienten besværet med at frigøre (lukke) ressourcen ved at eksponere definitionen af FILE og returnere den med værdi i fopen eller forvent meget af et ydeevnehøjde i betragtning af de relative omkostninger ved selve filhandlingerne for at undgå en bunktildeling.

Hotspots og rettelser

I andre tilfælde har jeg dog profileret en masse spild C-kode i ældre kodebaser med hotspots i malloc og unødvendige obligatoriske cache-savner som et resultat af at bruge denne praksis for ofte med uigennemsigtige markører og tildele unødvendigt for mange ting på bunken, nogle gange i store løkker.

En alternativ praksis, jeg bruger i stedet for, er at udsætte strukturdefinitioner, selvom klienten ikke er beregnet til at manipulere dem, ved at bruge en standard for navngivningskonvention til at kommunikere, at ingen andre skal røre ved felterne:

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

Hvis der er binære kompatibilitetsproblemer i fremtiden, så har jeg fundet det godt nok til bare overflødigt at reservere noget ekstra plads til fremtidige formål som sådan:

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

Denne reserverede plads er lidt spild, men kan være en livredder, hvis vi i fremtiden finder ud af, at vi skal tilføje nogle flere data til Foo uden at bryde de binære filer, der bruger vores bibliotek.

Efter min mening er skjuling af information og binær kompatibilitet typisk den eneste anstændige grund til kun at tillade bunktildeling af strukturer udover strukturer med variabel længde (som altid ville kræve det eller i det mindste være lidt akavet at bruge på anden måde, hvis klienten skulle allokere hukommelse på stakken i et VLA-mode ion til at tildele VLS). Selv store strukturer er ofte billigere at returnere efter værdi, hvis det betyder, at softwaren arbejder meget mere med den varme hukommelse på stakken. Og selvom de ikke var billigere at returnere ved værdi ved oprettelsen, kunne man bare gøre dette:

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

… for at initialisere Foo fra stakken uden mulighed for overflødig kopi. Eller klienten har endda friheden til at allokere Foo på bunken, hvis de ønsker det af en eller anden grund.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *