strcpy () restituisce il valore

Molte delle funzioni della libreria C standard, in particolare quelle per la manipolazione delle stringhe e in particolare strcpy (), condividono il seguente prototipo:

char *the_function (char *destination, ...) 

Il valore di ritorno di queste funzioni è in effetti lo stesso della destination fornita. Perché dovresti sprecare il valore di ritorno per qualcosa di ridondante? Ha più senso che una tale funzione sia nulla o restituisca qualcosa di utile.

La mia unica ipotesi sul perché questo è che è più facile e più conveniente annidare la chiamata di funzione in un’altra espressione, ad esempio:

 printf("%s\n", strcpy(dst, src)); 

Ci sono altri motivi validi per giustificare questo idioma?

    come ha fatto notare Evan, è ansible fare qualcosa di simile

     char* s = strcpy(malloc(10), "test"); 

    ad esempio, assegna un valore a malloc()ed memory, senza usare la variabile helper.

    (questo esempio non è il migliore, andrà in crash a causa di condizioni di memoria, ma l’idea è ovvia)

    Credo che la tua ipotesi sia corretta, rende più facile nidificare la chiamata.

    È anche estremamente facile da codificare.

    Il valore di ritorno è in genere lasciato nel registro AX (non è obbligatorio, ma è spesso il caso). E la destinazione viene inserita nel registro AX quando inizia la funzione. Per restituire la destinazione, il programmatore deve fare …. esattamente niente! Basta lasciare il valore dove è.

    Il programmatore potrebbe dichiarare la funzione come void . Ma quel valore di ritorno è già nel posto giusto, aspetta solo di essere restituito, e non costa nemmeno un’istruzione extra per restituirlo! Non importa quanto piccolo sia il miglioramento, in alcuni casi è utile.

    char *stpcpy(char *dest, const char *src); restituisce un puntatore alla fine della stringa e fa parte di POSIX.1-2008 . Prima di allora, era un’estensione GNU libc dal 1992. Se è apparso per la prima volta su Lattice C AmigaDOS nel 1986.

    gcc -O3 in alcuni casi ottimizzerà strcpy + strcat per usare la copia inline stpcpy o strlen +, vedi sotto.


    La libreria standard di C è stata progettata molto presto, ed è molto facile sostenere che le funzioni str* non siano progettate in modo ottimale. Le funzioni di I / O sono state progettate molto presto, nel 1972 prima che C avesse anche un preprocessore, motivo per cui fopen(3) utilizza una stringa di modalità invece di una bitmap di flag come Unix open(2) .

    Non sono stato in grado di trovare un elenco di funzioni incluse nel “pacchetto I / O portatile” di Mike Lesk, quindi non so se lo strcpy nella sua forma attuale risale a lì o se tali funzioni sono state aggiunte in seguito . (L’unica vera fonte che ho trovato è l’articolo C History di Dennis Ritchie , che è eccellente ma non approfondito. Non ho trovato alcuna documentazione o codice sorgente per l’effettivo pacchetto I / O stesso.)

    Appaiono nella loro forma attuale nella prima edizione di K & R , 1978.


    Le funzioni dovrebbero restituire il risultato del calcolo che fanno, se è potenzialmente utile per il chiamante, invece di buttarlo via . O come un puntatore alla fine della stringa o una lunghezza intera. (Un puntatore sarebbe naturale).

    Come dice @R:

    Tutti desideriamo che queste funzioni abbiano restituito un puntatore al byte null terminante (che ridurrebbe molte operazioni O(n) a O(1) )

    ad es. chiamare strcat(bigstr, newstr[i]) in un ciclo per creare una lunga stringa da molte stringhe corte (O (1)) ha una complessità di circa O(n^2) , ma strlen / memcpy guarderà solo a ciascuna carattere due volte (una volta in strlen, una volta in memcpy).

    Usando solo la libreria standard ANSI C, non c’è modo di guardare in modo efficiente ogni carattere una sola volta . È ansible scrivere manualmente un ciclo byte-at-a-time, ma per stringhe più lunghe di pochi byte, è peggio che guardare ogni carattere due volte con i compilatori correnti (che non auto-vettorizzano un ciclo di ricerca) sul moderno HW, dato un efficace SIMD strlen e memcpy forniti da libc. Si potrebbe usare length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length; length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length; , ma sprintf() deve analizzare la sua stringa di formato e non è veloce.

    Non esiste nemmeno una versione di strcmp o memcmp che restituisca la posizione della differenza . Se è quello che vuoi, hai lo stesso problema di Perché il confronto tra stringhe è così veloce in python? : una funzione di libreria ottimizzata che funziona più velocemente di qualsiasi cosa tu possa fare con un ciclo compilato (a meno che tu non abbia asm ottimizzato a mano per ogni piattaforma di destinazione che ti interessa), che puoi usare per avvicinarti al diverso byte prima di ricorrere a un ciclo regolare una volta che ti avvicini.

    Sembra che la libreria di stringhe di C sia stata progettata senza tener conto del costo O (n) di qualsiasi operazione, non solo di trovare la fine delle stringhe di lunghezza implicita, e il comportamento di strcpy non è sicuramente l’unico esempio.

    Fondamentalmente trattano le stringhe di lunghezza implicita come interi oggetti opachi, restituendo sempre puntatori all’inizio, mai alla fine o in una posizione all’interno di uno dopo la ricerca o l’aggiunta.


    Storia di congetture

    All’inizio di C su un PDP-11 , sospetto che strcpy non sia più efficiente di while(*dst++ = *src++) {} (e probabilmente è stato implementato in quel modo).

    Infatti, la prima edizione di K & R (pagina 101) mostra che l’implementazione di strcpy dice:

    Sebbene questo possa sembrare criptico a prima vista, la convenienza notazionale è considerevole, e l’idioma dovrebbe essere padroneggiato, se non altro per cui lo vedrete spesso nei programmi C.

    Ciò implica che si aspettavano completamente che i programmatori scrivessero i propri loop nei casi in cui si desiderava il valore finale di dst o src . E quindi forse non hanno visto la necessità di ridisegnare l’API della libreria standard fino a quando non era troppo tardi per esporre API più utili per le funzioni di libreria asm ottimizzate a mano.


    Ma restituire il valore originale di dst senso?

    strcpy(dst, src) restituire dst è analogo a x=y valuta la x . Quindi fa funzionare strcpy come un operatore di assegnazione di stringhe.

    Come indicato da altre risposte, ciò consente il nesting, come foo( strcpy(buf,input) ); . I primi computer erano molto limitati dalla memoria. Mantenere compatto il codice sorgente era una pratica comune . Punch card e terminali lenti erano probabilmente un fattore in questo. Non conosco gli standard storici di codifica o le guide di stile o quello che è stato considerato troppo da mettere su una riga.

    Anche i vecchi compilatori di Crusty erano forse un fattore. Con i moderni compilatori di ottimizzazione, char *tmp = foo(); / bar(tmp); non è più lento della bar(foo()); , ma è con gcc -O0 . Non so se i primissimi compilatori potrebbero ottimizzare le variabili completamente (senza riservare loro spazio per lo stack), ma si spera che almeno possano tenerle nei registri in casi semplici (a differenza del moderno gcc -O0 che di proposito rovescia / ricarica tutto per debugging coerente). per esempio gcc -O0 non è un buon modello per i compilatori antichi, perché è anti-optimizing di proposito per il debugging coerente.


    Possibile motivazione generata dal compilatore asm

    Data la mancanza di attenzione all’efficienza nella progettazione API generale della libreria di stringhe C, questo potrebbe essere improbabile. Ma forse c’era un vantaggio in termini di dimensioni del codice. (Nei primi computer, la dimensione del codice era più di un limite rigido rispetto al tempo della CPU).

    Non so molto sulla qualità dei primi compilatori C, ma è una scommessa sicura che non erano fantastici per l’ottimizzazione, anche per una semplice architettura semplice / ortogonale come PDP-11.

    È comune desiderare il puntatore di stringa dopo la chiamata alla funzione. Ad un livello asm, tu (il compilatore) probabilmente lo hai in un registro prima della chiamata. A seconda della convenzione di chiamata, lo si inserisce in pila o lo si copia nel registro corretto dove la convenzione di chiamata dice che il primo arg va. (vale a dire dove lo sta aspettando strcpy ). O se stai pianificando in anticipo, hai già il puntatore nel registro giusto per la convenzione di chiamata.

    Ma la funzione chiama clobber alcuni registri, inclusi tutti i registri di passaggio degli arg. Quindi, quando una funzione ottiene un arg in un registro, può incrementarlo lì invece di copiarlo in un registro scratch.

    Così come il chiamante, l’opzione code-gen per mantenere qualcosa attraverso una chiamata di funzione include:

    • memorizzarlo / ricaricarlo nella memoria dello stack locale. (Oppure ricaricalo se una copia aggiornata è ancora in memoria).
    • salva / ripristina un registro preservato dalla chiamata all’inizio / alla fine dell’intera funzione e copia il puntatore su uno di quei registri prima della chiamata alla funzione.
    • la funzione restituisce il valore in un registro per te. (Naturalmente, funziona solo se la sorgente C viene scritta per utilizzare il valore di ritorno anziché la variabile di input, ad esempio dst = strcpy(dst, src); se non lo si annida).

    Tutte le convenzioni di chiamata su tutte le architetture sono a conoscenza dei valori restituiti di dimensioni del puntatore restituite in un registro, quindi avere forse un’istruzione aggiuntiva nella funzione di libreria può salvare dimensioni del codice in tutti i chiamanti che desiderano utilizzare tale valore di ritorno.

    Probabilmente hai ottenuto asm dai primi primitivi compilatori C usando il valore di ritorno di strcpy (già in un registro) piuttosto che facendo in modo che il compilatore salvi il puntatore intorno alla chiamata in un registro conservato da una chiamata o lo riversi nello stack. Questo potrebbe essere ancora il caso.

    A proposito, su molti ISA, il registro del valore di ritorno non è il primo registro che passa l’arg. E a meno che non si utilizzino le modalità di indirizzamento dell’indice di base, costa un’istruzione extra (e si lega un altro registro) affinché strcpy copi il registro per un ciclo di incremento del puntatore.

    I toolchain di PDP-11 normalmente utilizzavano una sorta di convenzione di chiamata stack-args , spingendo sempre gli argomenti in pila. Non sono sicuro di quanti registri call-preserve vs. call-clobbered fossero normali, ma erano disponibili solo 5 o 6 regs GP ( R7 è il contatore del programma, R6 è il puntatore dello stack, R5 spesso usato come puntatore del frame ). Quindi è simile ma ancora più angusto rispetto a 32 bit x86.

     char *bar(char *dst, const char *str1, const char *str2) { //return strcat(strcat(strcpy(dst, str1), "separator"), str2); // more readable to modern eyes: dst = strcpy(dst, str1); dst = strcat(dst, "separator"); // dst = strcat(dst, str2); return dst; // simulates further use of dst } # x86 32-bit gcc output, optimized for size (not speed) # gcc8.1 -Os -fverbose-asm -m32 # input args are on the stack, above the return address push ebp # mov ebp, esp #, Create a stack frame. sub esp, 16 #, This looks like a missed optimization, wasted insn push DWORD PTR [ebp+12] # str1 push DWORD PTR [ebp+8] # dst call strcpy # add esp, 16 #, mov DWORD PTR [ebp+12], OFFSET FLAT:.LC0 # store new args over our incoming args mov DWORD PTR [ebp+8], eax # EAX = dst. leave jmp strcat # optimized tailcall of the last strcat 

    Questo è significativamente più compatto di una versione che non usa dst = , e riutilizza l’argomento di input per strcat . (Vedi entrambi sul explorer del compilatore Godbolt .)

    L’output -O3 è molto diverso: gcc per la versione che non usa il valore di ritorno usa stpcpy (restituisce un puntatore alla coda) e quindi mov -immediato per memorizzare i dati della stringa letterale direttamente nel posto giusto.

    Sfortunatamente, la versione dst = strcpy(dst, src) -O3 utilizza ancora strcpy normale, quindi inline strcat come strlen + mov -immediate.


    A C-string o meno a C-string

    Le stringhe di lunghezza implicita di C non sono sempre intrinsecamente cattive e presentano vantaggi interessanti (ad esempio un suffisso è anche una stringa valida, senza doverlo copiare).

    Ma la libreria di stringhe C non è progettata in modo da rendere ansible un codice efficiente, perché i loop char -at-time non si auto-realizzano in genere e le funzioni della libreria eliminano i risultati del lavoro che devono svolgere.

    gcc e clang non auto-vettorizzano mai cicli a meno che il conteggio dell’iterazione sia noto prima della prima iterazione, ad esempio for(int i=0; i . ICC è in grado di vettorizzare i loop di ricerca, ma è improbabile che lo faccia anche come scritto a mano asm.


    strncpy e così via sono fondamentalmente un disastro . es. strncpy non copia il terminante '\0' se raggiunge il limite della dimensione del buffer. Sembra che sia stato progettato per scrivere al centro di stringhe più grandi, non per evitare overflow del buffer. Non restituire un puntatore alla fine significa che devi arr[n] = 0; prima o dopo, potenzialmente toccando una pagina di memoria che non ha mai avuto bisogno di essere toccata.

    Alcune funzioni come snprintf sono utilizzabili e non terminano mai. Ricordare che cosa è difficile, e un rischio enorme se ricordi male, quindi devi controllare ogni volta che è importante per la correttezza.

    Come dice Bruce Dawson: smetti di usare strncpy già! . Apparentemente alcune estensioni MSVC come _snprintf sono anche peggio.

    Lo stesso concetto di Fluent Interfaces . Semplicemente rendendo il codice più veloce / più facile da leggere.

    Non penso che questo sia realmente impostato in questo modo per scopi di nidificazione, ma più per il controllo degli errori. Se la memoria non serve nessuna delle funzioni di libreria standard esegue molto il controllo degli errori da soli e quindi ha più senso che questo sia per determinare se qualcosa è andato storto durante la chiamata strcpy.

     if(strcpy(dest, source) == NULL) { // Something went horribly wrong, now we deal with it }