De ce multe funcții care returnează structuri în C, de fapt returnează indicatori către structuri?

Care este avantajul returnării unui pointer la o structură spre deosebire de returnarea întregii structuri în return declarația funcției?

Vorbesc despre funcții precum fopen și alte funcții de nivel scăzut, dar probabil că există funcții de nivel superior care returnează pointerii și structurilor.

Cred că aceasta este mai mult o alegere de proiectare decât o întrebare de programare și sunt curios să aflu mai multe despre avantajele și dezavantajele celor două metode.

Una dintre motive pentru care am crezut că este un avantaj pentru a returna un pointer la o structură este să poți spune mai ușor dacă funcția a eșuat prin returnarea NULL pointer.

Returnarea unei structuri complete care este NULL ar fi mai dificil, presupun sau mai puțin eficient. Este acesta un motiv valid?

Comentarii

  • @ JohnR.Strohm L-am încercat și de fapt funcționează. O funcție poate returna o structură …. Deci care este motivul pentru care nu se face?
  • Pre-standardizarea C nu a permis copierea structurilor sau transmiterea valorii. Biblioteca standard C are multe rezerve din acea epocă care nu ar fi scrise așa astăzi, de ex. a durat până la C11 pentru ca funcția gets() complet greșită să fie eliminată. Unii programatori au în continuare o aversiune față de copierea structurilor, obiceiurile vechi mor greu.
  • FILE* este efectiv un mâner opac. Codului utilizatorului nu ar trebui să îi pese de structura sa internă.
  • Revenirea prin referință este doar o valoare implicită rezonabilă atunci când aveți colectarea gunoiului.
  • @ JohnR.Strohm ” foarte senior ” în profilul dvs. pare să se întoarcă înainte de 1989 😉 – când ANSI C a permis ceea ce K & RC didn ‘ t: Copiați structurile în alocări, trecerea parametrilor și valorile returnate. Cartea originală a lui K & R ‘ s-a afirmat în mod explicit (eu ‘ m parafrazând): ” puteți face exact două lucruri cu o structură, puteți lua adresa cu & și accesați un membru cu .. ”

Răspuns

Acolo sunt mai multe motive practice pentru care funcții precum fopen returnează pointeri în loc de instanțe de tipuri struct:

  1. Doriți să ascundeți reprezentarea tipului struct de la utilizator;
  2. Alocați din nou un obiect în mod dinamic;
  3. Sunteți re referindu-se la o singură instanță a unui obiect prin referințe multiple;

În cazul unor tipuri precum FILE *, este „pentru că nu” doriți să expuneți utilizatorului detalii despre reprezentarea tipului – o FILE * obje ct servește ca un handle opac și pur și simplu îl treceți la diverse rutine I / O (și în timp ce FILE este adesea implementat ca struct tastați, nu trebuie să fie trebuie ).

Deci, puteți expune un tip incomplet struct într-un antet undeva:

typedef struct __some_internal_stream_implementation FILE; 

În timp ce nu puteți declara o instanță de tip incomplet, puteți declara un indicator către aceasta. Așa că pot crea un FILE * și îi pot atribui prin fopen, freopen etc. , dar nu pot manipula direct obiectul către care indică.

De asemenea, este probabil ca funcția fopen să aloce un FILE obiect dinamic, folosind malloc sau similar. În acest caz, are sens să returnăm un indicator.

În cele din urmă, este posibil să stocați un fel de stare într-un obiect struct și trebuie să faceți această stare disponibilă în mai multe locuri diferite. Dacă ați returnat instanțe de tip struct, aceste instanțe ar fi obiecte separate în memorie una de cealaltă și, în cele din urmă, ar ieși din sincronizare. Prin returnarea unui pointer la un singur obiect, toată lumea se referă la același obiect.

Comentarii

  • Un avantaj deosebit al utilizării indicatorului ca tipul opac este acela că structura în sine se poate schimba între versiunile bibliotecii și nu ‘ nu trebuie să recompilați apelanții.
  • @Barmar: Într-adevăr, stabilitatea ABI este imensul punct de vânzare al lui C și nu ar fi la fel de stabil fără indicatori opaci.

Răspuns

Există două moduri de„ returnare a unei structuri. ”Puteți returna o copie a datelor sau puteți returna o referință (pointer) la aceasta.În general, este de preferat să returnați (și să treceți în general) un pointer, din câteva motive.

În primul rând, copierea unei structuri necesită mult mai mult timp CPU decât copierea unui pointer. Dacă acesta este ceva codul dvs. face frecvent, poate provoca o diferență vizibilă de performanță.

În al doilea rând, indiferent de câte ori copiați un pointer în jur, acesta indică în continuare aceeași structură din memorie. Toate modificările aduse acestuia vor fi reflectate pe aceeași structură. Dar dacă copiați structura în sine și apoi faceți o modificare, modificarea apare doar pe copia respectivă . Orice cod care conține o copie diferită nu va fi modificat. Uneori, foarte rar, asta vrei, dar de cele mai multe ori nu este posibil și poate provoca erori dacă greșești.

Comentarii

  • Dezavantajul revenirii prin pointer: acum ‘ trebuie să urmăriți proprietatea asupra acelui obiect și posibil eliberează-l. De asemenea, indirectarea indicatorului poate fi mai costisitoare decât o copie rapidă. Există o mulțime de variabile aici, astfel încât utilizarea pointerelor nu este universal mai bună.
  • De asemenea, pointerele din zilele noastre sunt pe 64 de biți pe majoritatea platformelor desktop și server. Am ‘ am văzut mai mult de câteva structuri în cariera mea care s-ar încadra în 64 de biți. Deci, poți ‘ t întotdeauna spune că copierea unui pointer costă mai puțin decât copierea unei structuri.
  • Acesta este în mare parte un răspuns bun , dar nu sunt de acord cu partea uneori, foarte rar, asta vrei, dar de cele mai multe ori nu ‘ nu – chiar dimpotrivă. Returnarea unui indicator permite mai multe tipuri de efecte secundare nedorite și mai multe tipuri de modalități urâte de a greși proprietatea asupra unui indicator. În cazurile în care timpul procesorului nu este atât de important, prefer varianta de copiere, dacă aceasta este o opțiune, este mult mai puțin predispusă la erori.
  • Trebuie remarcat faptul că acest lucru este într-adevăr se aplică numai pentru API-urile externe. Pentru funcții interne, fiecare compilator, chiar marginal, din ultimele decenii, va rescrie o funcție care returnează o structură mare pentru a lua un pointer ca argument suplimentar și pentru a construi obiectul direct acolo. Argumentele despre imutabil vs mutabil au fost făcute suficient de des, dar cred că putem fi de acord cu toții că afirmația că structurile de date imuabile nu sunt aproape niciodată ceea ce vrei nu este adevărată.
  • De asemenea, ai putea menționa pereții de foc de compilare ca un profesionist pentru indicatori. În programele mari cu antete partajate pe scară largă, tipurile incomplete cu funcții împiedică necesitatea recompilării de fiecare dată când se modifică un detaliu de implementare. Un comportament mai bun de compilare este de fapt un efect secundar al încapsulării care se obține atunci când interfața și implementarea sunt separate. Revenirea (și trecerea, atribuirea) în funcție de valoare necesită informații de implementare.

Răspuns

În plus față de alte răspunsuri , uneori returnarea unui mic struct după valoare merită. De exemplu, s-ar putea returna o pereche de date și un cod de eroare (sau de succes) legat de acestea.

Pentru a lua un exemplu, fopen returnează doar o dată (FILE* deschisă) și în caz de eroare, dă codul de eroare prin errno pseudo-variabilă globală. Dar ar fi probabil mai bine să returnăm un struct a doi membri: mânerul FILE* și codul de eroare (care ar fi setat dacă mânerul fișierului este NULL). Din motive istorice, nu este cazul (iar erorile sunt raportate prin errno global, care astăzi este o macro).

Observați că Go language are o notație plăcută pentru a returna două (sau câteva) valori.

Observați, de asemenea, că pe Linux / x86-64 ABI și convențiile de apelare (consultați pagina pagina x86-psABI ) specifică faptul că un struct din doi membri scalari (de exemplu, un pointer și un număr întreg, sau doi indicatori, sau două numere întregi) este returnat prin două registre (și acest lucru este foarte eficient și nu merge prin memorie).

Deci, în noul cod C, returnarea unui C struct mic poate fi mai ușor de citit, mai ușor de utilizat și mai eficient.

Comentarii

  • De fapt, structurile mici sunt împachetate în rdx:rax. Deci, struct foo { int a,b; }; este returnat ambalat în rax (de exemplu, cu shift / or) și trebuie despachetat cu shift / mov. Aici ‘ este un exemplu despre Godbolt . Dar x86 poate utiliza cei 32 de biți mici ai unui registru pe 64 de biți pentru operațiuni pe 32 de biți fără să-i pese de biții mari, așa că ‘ este întotdeauna prea rău, dar cu siguranță mai rău decât utilizarea 2 înregistrează de cele mai multe ori pentru structuri cu 2 membri.
  • Corelate: bugs.llvm.org/show_bug.cgi? id = 34840 std::optional<int> returnează booleanul în jumătatea superioară a rax, deci aveți nevoie de o mască pe 64 de biți constantă pentru a-l testa cu test. Sau puteți utiliza bt. Dar e de rau pentru apelant și apelat să se compare cu utilizarea dl, ceea ce ar trebui să facă compilatoarele pentru ” private ” funcții. De asemenea, legat de: libstdc ++ ‘ s std::optional<T> isn ‘ t trivial copiat chiar și atunci când T este , deci revine întotdeauna prin pointer ascuns: stackoverflow.com/questions/46544019/… . (libc ++ ‘ s poate fi copiat în mod banal)
  • @PeterCordes: lucrurile dvs. conexe sunt C ++, nu C
  • Hopa, corect. Ei bine, același lucru s-ar aplica exact la struct { int a; _Bool b; }; în C, dacă apelantul ar dori să testeze booleanul, deoarece structurile C ++ care pot fi copiate în mod banal folosesc același ABI ca C.
  • Exemplu clasic div_t div()

Răspuns

Sunteți pe drumul cel bun

Ambele motive pe care le-ați menționat sunt valabile:

Unul dintre motivele pentru care am cred că asta ar fi un avantaj pentru a returna un pointer la o structură este să poți spune mai ușor dacă funcția a eșuat prin returnarea pointerului NULL.

Returnarea unei structuri FULL care este NUL ar fi mai dificil, presupun sau mai puțin eficiente. Este acesta un motiv valid?

Dacă aveți o textură (de exemplu) undeva în memorie și doriți să faceți referire la textura respectivă în mai multe locuri din program; nu ar fi înțelept să faci o copie de fiecare dată când ai vrut să o faci referință. În schimb, dacă treci pur și simplu în jurul unui indicator pentru a face referire la textură, programul tău va rula mult mai repede.

este alocarea dinamică a memoriei. De multe ori, când este compilat un program, nu sunteți sigur exact de câtă memorie aveți nevoie pentru anumite structuri de date. Când se întâmplă acest lucru, cantitatea de memorie pe care trebuie să o utilizați va fi determinată în timpul rulării. Puteți solicitați memorie folosind „malloc” și apoi eliberați-o când ați terminat de utilizat „liber”.

Un bun exemplu în acest sens este citirea dintr-un fișier specificat de utilizator. În acest caz, nu aveți idee cât de mare poate fi fișierul atunci când compilați programul. Puteți afla doar câtă memorie aveți nevoie atunci când programul rulează efectiv.

Atât indicatorii malloc, cât și cei liberi de întoarcere la locațiile din memorie. care folosesc alocarea dinamică a memoriei vor întoarce indicii către locul unde și-au creat structurile în memorie >

De asemenea, în comentarii văd că există o întrebare dacă puteți returna o structură dintr-o funcție. Puteți într-adevăr să faceți acest lucru. Următorul ar trebui să funcționeze:

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

Comentarii

  • Cum este posibil să nu știți câtă memorie o anumită variabilă va avea nevoie dacă aveți deja tipul struct definit?
  • @ JenniferAnderson C are un concept de tipuri incomplete: un nume de tip poate fi declarat, dar nu este încă definit, deci nu este disponibilă. Nu pot declara variabile de acest tip, dar pot declara pointeri la acel tip, de ex. struct incomplete* foo(void). În acest fel pot declara funcții într-un antet, dar pot defini doar structurile dintr-un fișier C, permițând astfel încapsularea.
  • @amon Deci așa se declară antetele funcției (prototipuri / semnături) înainte de a declara modul în care acestea se lucrează de fapt în C? Și este posibil să faceți același lucru structurilor și uniunilor din C
  • @JenniferAnderson declarați funcția prototipuri (funcții fără corpuri) în fișierele antet și apoi puteți apela acele funcții în alt cod, fără a cunoaște corpul funcțiilor, deoarece compilatorul trebuie doar să știe cum să aranjeze argumentele și cum să accepte valoarea returnată. Până când conectați programul, trebuie să cunoașteți funcția definiție (adică cu un corp), dar trebuie să procesați asta o singură dată. Dacă utilizați un tip non-simplu, trebuie să știe și structura de tip ‘, dar indicatorii au adesea aceeași dimensiune și nu ‘ nu contează utilizarea unui prototip ‘.

Răspuns

Ceva ca un FILE* nu este într-adevăr un pointer către o structură în ceea ce privește codul clientului, ci este în schimb o formă de identificator opac asociat cu unele altă entitate ca un fișier. Când un program apelează fopen, în general, nu-i pasă de niciunul dintre conținutul structurii returnate – tot ceea ce îi va pasa este că alte funcții precum fread vor face tot ce trebuie să facă cu aceasta.

Dacă o bibliotecă standard păstrează în FILE* informații despre de ex. poziția de citire curentă din acel fișier, un apel către fread ar trebui să poată actualiza aceste informații. Dacă primiți fread un pointer către FILE, este ușor. Dacă fread ar primi în schimb un FILE, nu ar avea nicio modalitate de actualizare a obiectului FILE deținut de apelant.

Răspuns

Ascunderea informațiilor

Care este avantajul returnării unui pointer la o structură spre deosebire de returnarea întregii structuri în declarația return a funcția?

Cea mai comună este ascunderea informațiilor . C nu are, să zicem, capacitatea de a face câmpurile private struct, darămite să furnizăm metode pentru a le accesa.

Deci, dacă doriți să forțați împiedicați dezvoltatorii să poată vedea și manipula conținutul unui pointee, cum ar fi FILE, atunci singura modalitate este de a preveni expunerea la definiția sa prin tratarea indicatorului ca opac a cărui dimensiune și definiție a pointei sunt necunoscute lumii exterioare. Definiția FILE va fi atunci vizibilă numai celor care implementează operațiunile care necesită definirea acesteia, cum ar fi fopen, în timp ce numai declarația de structură va fi vizibilă pentru antetul public.

Compatibilitate binară

Ascunderea definiției structurii poate ajuta, de asemenea, să ofere spațiu de respirație pentru a păstra compatibilitatea binară în API-urile dylib. Permite implementatorilor de biblioteci să schimbe câmpurile din structura opacă nu trebuie să rupă compatibilitatea binară cu cei care folosesc biblioteca, deoarece natura codului lor trebuie doar să știe ce pot face cu structura, nu cât de mare este sau ce câmpuri are.

de exemplu, pot rula de fapt câteva programe antice construite astăzi în epoca Windows 95 (nu întotdeauna perfect, dar surprinzător, multe încă funcționează). Șansele sunt că unele dintre codurile pentru acele binare antice au folosit indicatori opaci pentru structuri a căror dimensiune și conținut s-au schimbat de la era Windows 95. Cu toate acestea, programele continuă să funcționeze în versiuni noi de Windows, deoarece acestea nu au fost „expuse la conținutul acelor structuri. Când lucrați la o bibliotecă în care compatibilitatea binară este importantă, ceea ce clientul nu este expus este permis în general să se schimbe fără a se rupe compatibilitate înapoi.

Eficiență

Întoarcerea unei structuri complete care este NULL ar fi mai dificil, presupun sau mai puțin eficientă. Este acesta un motiv valabil?

Este de obicei mai puțin eficient presupunând că tipul se poate potrivi practic și poate fi alocat pe stivă, cu excepția cazului în care există, de obicei, mult mai puțin alocatorul de memorie generalizat fiind folosit în spatele scenei decât malloc, ca un alocator de memorie de dimensiuni fixe mai degrabă decât variabile, alocând deja memoria. Este un compromis de siguranță în acest caz, majoritatea probabil, pentru a permite dezvoltatorilor de biblioteci să mențină invarianți (garanții conceptuale) legate de FILE.

Nu este un motiv atât de valid, cel puțin din punct de vedere al performanței pentru a face ca fopen să returneze un pointer, deoarece singurul motiv pentru care „d returnează NULL este dacă nu se deschide un fișier. Aceasta ar fi optimizarea unui scenariu excepțional în schimbul încetinirii tuturor căilor de execuție obișnuite. S-ar putea să existe un motiv de productivitate valid în unele cazuri pentru a face proiectele mai simple pentru a le face să returneze pointeri pentru a permite returnarea NULL în anumite condiții post.

Pentru operațiile de fișiere, cheltuielile generale sunt relativ destul de banale în comparație cu operațiile de fișiere în sine, iar necesitatea manuală de a fclose nu poate fi oricum evitată. Deci, nu este de parcă am putea salva clientul dificultatea de a elibera (închide) resursa expunând definiția FILE și returnând-o după valoare în fopen sau vă așteptați la o mare creștere a performanței, având în vedere costul relativ al operațiunilor de fișiere în sine, pentru a evita o alocare a heap-ului.

Hotspots and Fixes

Totuși, pentru alte cazuri, am profilat o mulțime de cod C risipitor în bazele de cod vechi cu hotspoturi în malloc și cache-ul obligatoriu inutil ratează ca urmare a utilizării acestei practici prea frecvent cu indicatori opaci și a alocării inutile a mai multor lucruri în grămadă, uneori în bucle mari.

O practică alternativă pe care o folosesc în schimb este să expun definițiile structurii, chiar dacă clientul nu este menit să le manipuleze, utilizând un standard de convenție de denumire pentru a comunica că nimeni altcineva nu ar trebui să atingă câmpurile:

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

Dacă există probleme de compatibilitate binară în viitor, atunci am considerat că este suficient de bun să rezerve superfluu un spațiu suplimentar pentru scopuri viitoare, cum ar fi:

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

Acel spațiu rezervat este puțin risipitor, dar poate fi un economisitor de viață dacă descoperim în viitor că trebuie să adăugăm câteva date la Foo fără a sparge binarele care utilizează biblioteca noastră.

În opinia mea, ascunderea informațiilor și compatibilitatea binară sunt de obicei singurul motiv decent pentru a permite doar alocarea heap a structuri pe lângă structuri cu lungime variabilă (care ar necesita întotdeauna acest lucru, sau cel puțin ar fi puțin cam incomod pentru a fi utilizate altfel dacă clientul ar trebui să aloce memorie pe stivă într-o fash VLA ion pentru a aloca VLS). Chiar și structurile mari sunt adesea mai ieftine de returnat în funcție de valoare dacă acest lucru înseamnă că software-ul funcționează mult mai mult cu memoria fierbinte din stivă. Și chiar dacă nu ar fi „mai ieftin de returnat în funcție de valoare la creație, s-ar putea face pur și simplu acest lucru:

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

… pentru a inițializa Foo din stivă fără posibilitatea unei copii inutile. Sau clientul are chiar și libertatea de a aloca Foo pe heap dacă doresc dintr-un anumit motiv.

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *