Funkcje zwracające ciągi, dobry styl?

W moich programach w języku C często potrzebuję sposobu, aby stworzyć reprezentację łańcuchową moich ADT. Nawet jeśli nie muszę w żaden sposób drukować ciągu na screen, dobrze jest mieć taką metodę debugowania. Dlatego często pojawia się taka funkcja.

char * mytype_to_string( const mytype_t *t ); 

Właściwie zdaję sobie sprawę, że mam (co najmniej) trzy opcje obsługi pamięci dla zwracanego ciągu.

Alternatywa 1: Przechowywanie ciągu zwrotnego w statycznej tablicy znaków w funkcji Nie potrzebuję dużo myślenia, poza tym, że ciąg jest nadpisywany przy każdym wywołaniu. W niektórych przypadkach może to być problem.

Alternatywa 2: przydziel ciąg na stercie za pomocą malloc wewnątrz funkcji. Naprawdę fajne, ponieważ wtedy nie będę musiał myśleć o rozmiarze bufora ani nadpisywaniu. Jednak muszę pamiętać, aby zwolnić () ciąg po zakończeniu, a następnie muszę również przypisać do zmiennej tymczasowej takiej, że Mogę zwolnić. I wtedy alokacja sterty jest naprawdę znacznie wolniejsza niż alokacja stosu, dlatego może być wąskim gardłem, jeśli zostanie to powtórzone w pętli.

Alternatywa 3: Przekaż wskaźnik do bufora i pozwól wywołującemu przydzielić Na przykład:

char * mytype_to_string( const mytype_t *mt, char *buf, size_t buflen ); 

To wymaga większego wysiłku dla dzwoniącego. Zauważyłem również, że ta alternatywa daje mi inną opcję w kolejności argumentów. Który argument powinienem mieć pierwszy i ostatni? (Właściwie sześć możliwości)

Którą więc wolę preferować? Dlaczego? Czy istnieje jakiś niepisany standard wśród programistów C?

Komentarze

  • Uwaga obserwacyjna, większość systemów operacyjnych używa opcji 3 – wywołujący i tak przydziela bufor; informuje wskaźnik bufora i pojemność; wywoływany wypełnia buff er, a także zwraca rzeczywistą długość łańcucha , jeśli bufor jest niewystarczający. Przykład: sysctlbyname w systemie OS X i iOS

Odpowiedź

Metody, które widziałem najczęściej, to 2 i 3.

Bufor dostarczony przez użytkownika jest w rzeczywistości dość prosty w użyciu:

char[128] buffer; mytype_to_string(mt, buffer, 128); 

Chociaż większość implementacji zwróci ilość używanego bufora.

Opcja 2 będzie wolniejsza i niebezpieczna w przypadku korzystania z dynamicznie połączonych bibliotek, w których mogą używać różne środowiska wykonawcze (i różne stosy). Nie możesz więc zwolnić tego, co zostało pobrane w innej bibliotece. To wymaga funkcji free_string(char*), aby sobie z tym poradzić.

Komentarze

  • Dzięki! Myślę, że ja też najbardziej lubię Alternative 3. Jednak chcę mieć możliwość wykonywania takich czynności, jak: printf("MyType: %s\n", mytype_to_string( mt, buf, sizeof(buf));, dlatego ' nie chciałbym zwracać użytej długości, ale raczej wskaźnika do ciągu. Komentarz dynamicznej biblioteki jest naprawdę ważny.
  • Nie powinien ' t to być sizeof(buffer) - 1, aby spełnić \0 terminator?
  • @ Michael-O nie, w rozmiarze bufora jest zawarty termin pusty, co oznacza, że maksymalny ciąg, który można wstawić, jest o 1 mniejszy niż przekazany rozmiar. To jest wzorzec, z którego korzysta bezpieczny ciąg znaków w standardowej bibliotece, taki jak snprintf.
  • @ratchetfreak Dziękujemy za wyjaśnienie. Byłoby miło rozszerzyć odpowiedź o tę mądrość.

Odpowiedź

Dodatkowy pomysł na projekt # 3

Jeśli to możliwe, podaj także maksymalny rozmiar potrzebny dla mytype w tym samym pliku .h co mytype_to_string().

#define MYTYPE_TO_STRING_SIZE 256 

Teraz użytkownik może odpowiednio kodować.

char buf[MYTYPE_TO_STRING_SIZE]; puts(mytype_to_string(mt, buf, sizeof buf)); 

Zamówienie

Rozmiar tablic na początku pozwala na typy VLA.

char * mytype_to_string( const mytype_t *mt, size_t bufsize, char *buf[bufsize]); 

Nie tak ważne przy pojedynczym wymiarze, ale przydatne przy 2 lub więcej.

void matrix(size_t row, size_t col, double matrix[row][col]); 

Przypominam sobie, że w następnym rozdziale preferowanym idiomem jest umieszczanie rozmiaru jako pierwszego. Muszę znaleźć to odniesienie ….

Odpowiedź

Jako dodatek do doskonałej odpowiedzi @ratchetfreak „, chciałbym zwrócić uwagę, że alternatywa nr 3 opiera się na podobnym paradygmacie / wzorcu, co standardowe funkcje biblioteki C.

Na przykład strncpy.

 char * strncpy ( char * destination, const char * source, size_t num );  

Postępowanie zgodnie z tym samym paradygmatem mogłoby pomóc aby zmniejszyć obciążenie poznawcze nowych programistów (lub nawet Ciebie w przyszłości), gdy będą musieli użyć Twojej funkcji.

Jedyną różnicą w stosunku do tego, co masz w swoim poście, jest to, że destination w bibliotekach C jest zwykle wymieniany jako pierwszy na liście argumentów.A więc:

 char * mytype_to_string( char *buf, const mytype_t *mt, size_t buflen );  

Odpowiedź

I „d echo @ratchet_freak w większości przypadków (być może z niewielką poprawką dla sizeof buffer ponad 128) ale chcę wskoczyć tutaj z dziwną odpowiedzią. A co powiesz na bycie dziwnym? Dlaczego nie, poza problemami związanymi z otrzymywaniem dziwnych spojrzeń od naszych kolegów i koniecznością dodatkowego przekonywania? I oferuję to:

// Note allocator parameter. char* mytype_to_string(allocator* alloc, const mytype_t* t) { char* buf = allocate(alloc, however_much_you_need); // fill out buf based on "t" contents return buf; } 

I przykład użycia:

void func(my_type a, my_type b) { allocator alloc = allocator_new(); const char* str1 = mytype_to_string(&alloc, &a); if (!str1) goto oom; const char* str2 = mytype_to_string(&alloc, &b); if (!str2) goto oom // do something with str1 and str2 goto finish; oom: errno = ENOMEM; finish: // Frees all memory allocated through `alloc`. allocator_purge(&alloc); } 

Jeśli zrobiłeś to w ten sposób, możesz uczynić swój alokator bardzo wydajnym (bardziej wydajnym niż malloc zarówno pod względem kosztów alokacji / zwalniania, jak i ulepszonej lokalizacji odniesienia dla dostępu do pamięci). Może to być alokator areny, który w typowych przypadkach wymaga zwiększenia wskaźnika dla żądań alokacji i puli pamięci w sposób sekwencyjny z dużych, ciągłych bloków (przy czym pierwszy blok nie wymaga nawet sterty al lokalizacja – może być przydzielona na stosie). Upraszcza obsługę błędów. Ponadto, i to może być najbardziej dyskusyjne, ale myślę, że jest to praktycznie całkiem oczywiste, jeśli chodzi o sposób, w jaki wyjaśnia wywołującemu, że zamierza przydzielić pamięć do przekazanego alokatora, wymagając jawnego zwolnienia (allocator_purge w tym przypadku) bez konieczności dokumentowania takiego zachowania w każdej możliwej funkcji, jeśli konsekwentnie używasz tego stylu. Akceptacja parametru alokatora sprawia, że mam nadzieję, że jest to całkiem oczywiste.

Nie wiem. Dostaję tutaj kontrargumenty, takie jak implementacja najprostszego możliwego przydzielającego areny (po prostu użyj maksymalnego wyrównania dla wszystkich żądań) i radzenie sobie z tym to zbyt dużo pracy. Moja bezpośrednia myśl jest taka, że kim jesteśmy, programiści Pythona? Jeśli tak, to moglibyśmy równie dobrze użyć Pythona. Jeśli te szczegóły nie mają znaczenia, użyj Pythona. Mówię poważnie. Miałem wielu kolegów programistów C, którzy najprawdopodobniej napisaliby nie tylko poprawniejszy kod, ale być może nawet bardziej efektywny w Pythonie, ponieważ ignorują takie rzeczy, jak lokalność odniesienia, potykając się o błędy, które tworzą po lewej i prawej stronie. Widzę, co jest takiego przerażającego w jakimś prostym alokatorze areny, jeśli jesteśmy programistami C zainteresowanymi takimi rzeczami, jak lokalizacja danych i optymalny wybór instrukcji, a jest to prawdopodobnie o wiele mniej do myślenia niż przynajmniej typy interfejsów, które wymagają wywołujące, aby jawnie zwolnić każdą pojedynczą rzecz, którą interfejs może zwrócić. Oferuje masowe cofnięcie przydziału zamiast indywidualnego cofnięcia przydziału, który jest bardziej podatny na błędy. Właściwy programista C kwestionuje wywołania loopy do malloc, tak jak ja to widzę, zwłaszcza gdy pojawia się jako punkt aktywny w ich profilerze. Z mojego punktu widzenia, musi być więcej ” oomph ” do uzasadnienia, aby nadal być programistą C w 2020 roku, a możemy ” Nie unikaj już takich rzeczy jak alokatory pamięci.

I nie ma to skrajnych przypadków przydzielania bufora o stałej wielkości, w którym przydzielamy, powiedzmy, 256 bajtów, a wynikowy ciąg jest większy. Nawet jeśli nasze funkcje unikają przepełnienia bufora (jak w przypadku sprintf_s), jest więcej kodu do prawidłowego odzyskania po takich błędach, które są wymagane, które możemy pominąć w przypadku alokatora, ponieważ tak nie jest „Nie mamy tych skrajnych przypadków. Nie musimy zajmować się takimi przypadkami w przypadku alokatora, chyba że naprawdę wyczerpujemy fizyczną przestrzeń adresową naszego sprzętu (którą obsługuje powyższy kod, ale nie ma do czynienia z ” z wstępnie przydzielonego bufora ” oddzielnie od ” z pamięci „).

Odpowiedź

Poza tym, że to, co proponujesz, to nieprzyjemny zapach kodu, alternatywa 3 brzmi dla mnie najlepiej. Myślę też, jak @ gnasher729, że używasz niewłaściwego języka.

Komentarze

Odpowiedź

Szczerze mówiąc, możesz chcieć przełączyć się na inny język, w którym zwracanie ciągu znaków nie jest złożoną, pracochłonną i podatną na błędy operacją.

Możesz rozważyć C ++ lub Objective-C, w którym 99% kodu pozostanie niezmienione.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *