Perché abbiamo un incremento postfisso?

55

Disclaimer : conosco perfettamente la semantica di incremento prefisso e postfisso. Quindi per favore non spiegarmi come funzionano.

Leggendo le domande sullo stack overflow, non posso fare a meno di notare che i programmatori vengono confusi dall'operatore di incremento postfisso più e più volte. Da ciò deriva la seguente domanda: esiste un caso d'uso in cui l'incremento postfisso fornisce un vantaggio reale in termini di qualità del codice?

Permettimi di chiarire la mia domanda con un esempio. Ecco un'implementazione super-tersa di strcpy :

while (*dst++ = *src++);

Ma questo non è esattamente il codice più autoreferenziale del mio libro (e produce due fastidiosi avvertimenti su compilatori sane). Allora, cosa c'è di sbagliato con la seguente alternativa?

while (*dst = *src)
{
    ++src;
    ++dst;
}

Possiamo quindi sbarazzarci del compito di confusione nella condizione e ottenere un codice completamente privo di avvisi:

while (*src != '
while (*dst++ = *src++);
') { *dst = *src; ++src; ++dst; } *dst = '
while (*dst = *src)
{
    ++src;
    ++dst;
}
';

(Sì, lo so, src e dst avranno valori finali diversi in queste soluzioni alternative, ma poiché strcpy torna immediatamente dopo il ciclo, in questo caso non ha importanza.)

Sembra che lo scopo dell'aumento postfisso sia quello di rendere il codice il più conciso possibile. Semplicemente non riesco a vedere come questo sia qualcosa per cui dovremmo lottare. Se questo era originariamente sulle prestazioni, è ancora attuale oggi?

    
posta fredoverflow 01.02.2011 - 00:59
fonte

15 risposte

23

Anche se una volta ha avuto alcune implicazioni sulle prestazioni, penso che la vera ragione sia quella di esprimere il tuo intento in modo pulito. La vera domanda è se qualcosa while (*d++=*s++); esprime chiaramente l'intenzione o meno. IMO, lo fa, e trovo le alternative che offri meno chiare - ma ciò può (facilmente) essere il risultato di aver speso decenni ad abituarsi a come sono fatte le cose. Avere imparato C da K & R (perché ci erano quasi nessun altro libro su C al momento) probabilmente aiuta anche.

In una certa misura, è vero che la precisione è stata valutata in misura molto maggiore nel vecchio codice. Personalmente, penso che questa sia stata in gran parte una buona cosa: capire alcune righe di codice è solitamente abbastanza banale; ciò che è difficile è capire grandi pezzi di codice. Test e studi hanno dimostrato ripetutamente che montare tutto il codice sullo schermo in una sola volta è un fattore importante nella comprensione del codice. Quando le schermate si espandono, questo sembra rimanere vero, quindi mantenere il codice (ragionevolmente) terso rimane prezioso.

Ovviamente è possibile esagerare, ma non penso che sia così. In particolare, penso che esagerare quando la comprensione di una singola riga di codice diventa estremamente difficile o dispendiosa in termini di tempo - in particolare, quando la comprensione di un minor numero di righe di codice richiede uno sforzo maggiore rispetto alla comprensione di più linee. È frequente in Lisp e in APL, ma non sembra (almeno per me) essere il caso qui.

Sono meno preoccupato per gli avvertimenti del compilatore - è la mia esperienza che molti compilatori emettono avvertimenti assolutamente ridicoli su base abbastanza regolare. Anche se penso che la gente dovrebbe capire il loro codice (e qualsiasi avvertenza che potrebbe produrre), il codice decente che fa scattare un avvertimento in alcuni compilatori non è necessariamente sbagliato. Certo, i principianti non sempre sanno cosa possono tranquillamente ignorare, ma noi non restiamo principianti per sempre, e non abbiamo bisogno di codice come lo siamo noi.

    
risposta data 01.02.2011 - 01:30
fonte
32

È, err, era, una cosa hardware

Interessante che tu possa notare. L'incremento di Postfix è probabilmente lì per un numero di motivi perfettamente validi, ma come molte cose in C, è popolarità può essere tracciato all'origine della lingua.

Sebbene C fosse sviluppato su una varietà di macchine vecchie e poco potenti, C e Unix colsero per la prima volta il tempo relativo relativo con i modelli gestiti dalla memoria del PDP-11. Questi erano computer relativamente importanti ai loro tempi e Unix era di gran lunga migliore - esponenzialmente migliore - degli altri 7 sistemi operativi scadenti disponibili per il -11.

E, così accade, sul PDP-11,

*--p

e

*p++

... sono stati implementati in hardware come modalità di indirizzamento. (Anche *p , ma nessun'altra combinazione.) Su quelle macchine precedenti, tutto meno di 0,001 GHz, il salvataggio di un'istruzione o due in un ciclo doveva essere quasi un aspetta un secondo o aspetta una differenza di minuto o di uscita per pranzo. Questo in particolare non parla specificamente del postincremento, ma un loop con post-puntamento del puntatore avrebbe potuto essere molto meglio dell'indicizzazione di quel momento.

Di conseguenza, i modelli di progettazione perché gli idiomi C diventavano macro C mentale.

È qualcosa come dichiarare le variabili subito dopo un { ... non dal momento che C89 era corrente è stato un requisito, ma ora è un modello di codice.

Aggiornamento: Ovviamente, il motivo principale per cui *p++ è nella lingua è perché è esattamente ciò che si vuole fare spesso. La popolarità del modello di codice è stata rinforzata dall'hardware popolare che è arrivato e ha abbinato il modello già esistente di un linguaggio progettato leggermente prima dell'arrivo del PDP-11.

In questi giorni non fa alcuna differenza quale pattern usi, o se usi l'indicizzazione, e di solito programmiamo comunque ad un livello più alto, ma deve avere molta importanza su quelle macchine da 0,001 GHz, e usare qualcosa di diverso da *--x o *x++ avrebbe significato che non avevi "ottenuto" il PDP-11, e potresti avere persone che si avvicinano a te e che dicono "lo sapevi che ..." :-): -)

    
risposta data 01.02.2011 - 01:11
fonte
14

Gli operatori prefisso e suffisso -- e ++ sono stati introdotti nel linguaggio B (predecessore di C) di Ken Thompson - e no, non erano ispirati al PDP-11, che non esisteva al tempo.

Citando "Lo sviluppo del linguaggio C" di Dennis Ritchie :

Thompson went a step further by inventing the ++ and -- operators, which increment or decrement; their prefix or postfix position determines whether the alteration occurs before or after noting the value of the operand. They were not in the earliest versions of B, but appeared along the way. People often guess that they were created to use the auto-increment and auto-decrement address modes provided by the DEC PDP-11 on which C and Unix first became popular. This is historically impossible, since there was no PDP-11 when B was developed. The PDP-7, however, did have a few ‘auto-increment’ memory cells, with the property that an indirect memory reference through them incremented the cell. This feature probably suggested such operators to Thompson; the generalization to make them both prefix and postfix was his own. Indeed, the auto-increment cells were not used directly in implementation of the operators, and a stronger motivation for the innovation was probably his observation that the translation of ++x was smaller than that of x=x+1.

    
risposta data 24.09.2016 - 02:53
fonte
6

L'ovvia ragione per cui l'operatore postincremento esiste è che non devi scrivere espressioni come (++x,x-1) o (x+=1,x-1) in tutto il luogo o separare inutilmente dichiarazioni banali con effetti collaterali di facile comprensione in più dichiarazioni. Ecco alcuni esempi:

while (*s) x+=*s++-'0';

if (x) *s++='.';

Ogni volta che si legge o si scrive una stringa nella direzione in avanti, il postincremento e non il pre-incremento, è quasi sempre l'operazione naturale. Non ho quasi mai incontrato usi del mondo reale per il preincremento, e l'unico uso principale che ho trovato per il predecrement è la conversione di numeri in stringhe (perché i sistemi di scrittura umana scrivono numeri indietro).

Modifica: In realtà non sarebbe così brutto; invece di (++x,x-1) potresti ovviamente usare ++x-1 o (x+=1)-1 . Ancora x++ è più leggibile.

    
risposta data 01.02.2011 - 02:08
fonte
4

Il PDP-11 ha offerto operazioni di post-incremento e pre-decremento nel set di istruzioni. Ora queste non erano istruzioni. Erano dei modificatori di istruzioni che ti permettevano di specificare che volevi il valore di un registro prima che fosse incrementato o dopo che era stato decrementato.

Ecco un passaggio di word-copy nel linguaggio macchina:

movw (r1)++,(r2)++

che spostava una parola da una posizione a un'altra, puntata da registri, e i registri sarebbero pronti a rifarlo.

Poiché C è stato sviluppato e per il PDP-11, molti concetti utili sono stati introdotti in C. C era destinato a essere un utile sostituto del linguaggio assemblatore. Pre-incremento e post-decremento sono stati aggiunti per simmetria.

    
risposta data 23.09.2016 - 23:14
fonte
3

Dubito che sia mai stato davvero necessario. Per quanto ne so, non si compila in qualcosa di più compatto sulla maggior parte delle piattaforme rispetto all'utilizzo del pre-incremento come hai fatto tu, nel ciclo. Era solo che al momento in cui è stato creato, la chiarezza del codice era più importante della chiarezza del codice.

Questo non vuol dire che la chiarezza del codice non sia (o non fosse) importante, ma quando stavi scrivendo su un modem a bassa velocità (qualsiasi cosa in cui la misura in baud è lenta) dove ogni battitura doveva rendilo al mainframe e poi tornare indietro di un byte alla volta con un singolo bit usato come controllo di parità, non volevi scrivere molto.

Questo è un po 'come il & e | operatore con precedenza inferiore a ==

È stato progettato con le migliori intenzioni (una versione non-cortocircuitante di & e ||), ma ora confonde i programmatori ogni giorno, e probabilmente non cambierà mai.

Ad ogni modo, questa è solo una risposta in quanto penso che non ci sia una buona risposta alla tua domanda, ma probabilmente sarò smentito da un altro programmatore di guru di me.

- EDIT -

Devo notare che I trovo che sia incredibilmente utile, sto solo sottolineando che non cambierà se piaccia o no a qualcuno.

    
risposta data 01.02.2011 - 01:08
fonte
3

Mi piace l'aperitivo postfatto quando si tratta di puntatori (indipendentemente dal fatto che li stia tagliando) perché

p++

si legge più naturalmente come "sposta al punto successivo" rispetto all'equivalente

p += 1

che sicuramente deve confondere i principianti la prima volta che li vedono usati con un puntatore in cui sizeof (* p)! = 1.

Sei sicuro di affermare correttamente il problema di confusione? Non è la cosa che confonde i principianti il fatto che l'operatore postfix ++ abbia una precedenza più alta rispetto all'operatore dereference *, così che

*p++

analizza come

*(p++)

e non

(*p)++

come ci si potrebbe aspettare?

(Pensi che sia male? Vedi Leggere le dichiarazioni C .)

    
risposta data 01.02.2011 - 03:08
fonte
2

Tra gli elementi sottili della buona programmazione ci sono localizzazione e minimalismo :

  • inserire le variabili in un ambito di utilizzo minimo
  • utilizzando const quando l'accesso in scrittura non è richiesto
  • ecc.

Nello stesso spirito, x++ può essere visto come un modo per localizzare un riferimento al valore corrente di x mentre indica immediatamente che hai - almeno per ora - finito con quel valore e vuoi spostarti a quello successivo (valido se x è un int o un puntatore o iteratore). Con un po 'di immaginazione, potresti paragonarlo a lasciare che il vecchio valore / posizione di x diventi "fuori ambito" immediatamente dopo che non è più necessario e passando al nuovo valore. Il punto preciso in cui tale transizione è possibile è evidenziato dal suffisso ++ . L'implicazione che questo è probabilmente l'uso finale del "vecchio" x può essere una preziosa visione dell'algoritmo, che assiste il programmatore mentre scansiona il codice circostante. Ovviamente, il suffisso ++ può mettere il programmatore in cerca di usi del nuovo valore, che può essere o meno buono a seconda di quando quel nuovo valore è effettivamente necessario, quindi è un aspetto della "arte" o " craft "di programmazione per determinare cosa è più utile nelle circostanze.

Mentre molti principianti possono essere confusi da una caratteristica, vale la pena di bilanciare ciò con i benefici a lungo termine: i principianti non restano a lungo i principianti.

    
risposta data 01.02.2011 - 03:36
fonte
2

Wow, molte risposte non proprio sul punto (se posso essere così audace), e per favore perdonami se sottolineo l'ovvio - specialmente alla luce del tuo commento per non sottolineare la semantica, ma l'ovvio (dal punto di vista di Stroustrup, suppongo) non sembra ancora essere stato pubblicato! :)

Postfix x++ produce un temporaneo che viene passato "verso l'alto" nell'espressione, mentre la variabile x viene successivamente incrementata.

Prefix ++x non produce un oggetto temporaneo, ma incrementa 'x' e passa il risultato all'espressione.

Il valore è conveniente, per coloro che conoscono l'operatore.

Ad esempio:

int x = 1;
foo(x++); // == foo(1)
// x == 2: true

x = 1;
foo(++x); // == foo(2)
// x == 2: true

Ovviamente, i risultati di questo esempio possono essere prodotti da un altro codice equivalente (e forse meno obliquo).

Quindi perché abbiamo l'operatore postfisso? Immagino perché è un idioma che è persistito, nonostante la confusione che ovviamente crea. È un costrutto ereditario che una volta veniva percepito come un valore, anche se non sono sicuro che il valore percepito fosse tanto per le prestazioni quanto per la comodità. Questa convenienza non è stata persa, ma penso che sia aumentato l'apprezzamento della leggibilità, con conseguente interrogatorio sullo scopo dell'operatore.

Abbiamo più bisogno dell'operatore postfix? Probabilmente no. Il risultato è confuso e crea una barriera alla comprensibilità. Alcuni bravi programmatori sapranno immediatamente dove trovarlo utile, spesso in luoghi dove ha una bellezza particolarmente "PERL-esque". Il costo di quella bellezza è la leggibilità.

Sono d'accordo sul fatto che il codice esplicito ha vantaggi in termini di chiarezza, leggibilità, comprensibilità e manutenibilità. I buoni designer e manager lo vogliono.

Tuttavia, c'è una certa bellezza che alcuni programmatori riconoscono che li incoraggia a produrre codice con cose puramente belle per semplicità - come l'operatore postfisso - quale codice non avrebbero mai pensato altrimenti - o trovato stimolante intellettualmente. C'è una certa cosa che lo rende più bello, se non più desiderabile, nonostante la pithiness.

In altre parole, alcune persone trovano while (*dst++ = *src++); semplicemente per essere una soluzione più bella, qualcosa che toglie il respiro con la sua semplicità, proprio come se fosse un pennello su tela. Il fatto che sei costretto a capire il linguaggio per apprezzare la bellezza non fa che aumentare la sua magnificenza.

    
risposta data 01.02.2011 - 05:04
fonte
2

Diamo un'occhiata a Kernighan & Giustificazione originale di Ritchie (pagina originale K & R 42 e 43):

The unusual aspects is that ++ and -- may be used either as prefix or as postfix. (...) In the context where no value is wanted (..) choose prefix or postfix according to taste. But htere are situations where one or the other is specifically called for.

Il testo continua con alcuni esempi che utilizzano incrementi all'interno dell'indice, con l'obiettivo esplicito di scrivere il codice " più compatto ". Quindi la ragione dietro a questi operatori è la convenienza di un codice più compatto.

I tre esempi forniti ( squeeze() , getline() e strcat() ) usano solo postfix all'interno di espressioni usando l'indicizzazione. Gli autori confrontano il codice con una versione più lunga che non utilizza incrementi incorporati. Ciò conferma che l'attenzione è sulla compattezza.

Evidenzia K & R a pagina 102, l'uso di questi operatori in combinazione con la dereferenziazione del puntatore (ad esempio *--p e *p-- ). Non viene fornito alcun altro esempio, ma, di nuovo, chiariscono che il vantaggio è la compattezza.

Il prefisso è ad esempio usato molto comunemente quando si decrementa un indice o un puntatore che inizia dalla fine.

 int i=0; 
 while (i<10) 
     doit (i++);  // calls function for 0 to 9  
                  // i is 10 at this stage
 while (--i >=0) 
     doit (i);    // calls function from 9 to 0 
    
risposta data 24.09.2016 - 00:17
fonte
1

Non sono d'accordo con la premessa che ++p , p++ , sia in qualche modo difficile da leggere o poco chiaro. Uno significa "incrementa p e poi legge p", l'altro significa "leggi p e poi incrementa p". In entrambi i casi, l'operatore nel codice è esattamente dove si trova nella spiegazione del codice, quindi se sai cosa significa ++ , sai cosa significa il codice risultante.

Puoi creare codice offuscato usando la sintassi any , ma non vedo qui un caso in cui p++ / ++p è intrinsecamente offuscato.

    
risposta data 01.02.2011 - 04:59
fonte
1

questo è dal punto di vista di un ingegnere elettrico:

ci sono molti processori con operatori incorporati post-incremento e pre-decremento allo scopo di mantenere uno stack LIFO (last-in-first-out).

potrebbe essere come:

 float stack[4096];
 int stack_pointer = 0;

 ... 

 #define push_stack(arg) stack[stack_pointer++] = arg;
 #define pop_stack(arg) arg = stack[--stack_pointer];

 ...

Non lo so, ma questo è il motivo per cui mi aspetterei di vedere entrambi gli operatori di incremento prefisso e postfix.

    
risposta data 24.09.2016 - 01:55
fonte
1

Per sottolineare il punto di Christophe in merito alla compattezza, voglio mostrarti del codice dal V6. Questo fa parte del compilatore C originale, da link :

/*
 * Look up the identifier in symbuf in the symbol table.
 * If it hashes to the same spot as a keyword, try the keyword table
 * first.  An initial "." is ignored in the hash.
 * Return is a ptr to the symbol table entry.
 */
lookup()
{
    int ihash;
    register struct hshtab *rp;
    register char *sp, *np;

    ihash = 0;
    sp = symbuf;
    if (*sp=='.')
        sp++;
    while (sp<symbuf+ncps)
        ihash =+ *sp++;
    rp = &hshtab[ihash%hshsiz];
    if (rp->hflag&FKEYW)
        if (findkw())
            return(KEYW);
    while (*(np = rp->name)) {
        for (sp=symbuf; sp<symbuf+ncps;)
            if (*np++ != *sp++)
                goto no;
        csym = rp;
        return(NAME);
    no:
        if (++rp >= &hshtab[hshsiz])
            rp = hshtab;
    }
    if(++hshused >= hshsiz) {
        error("Symbol table overflow");
        exit(1);
    }
    rp->hclass = 0;
    rp->htype = 0;
    rp->hoffset = 0;
    rp->dimp = 0;
    rp->hflag =| xdflg;
    sp = symbuf;
    for (np=rp->name; sp<symbuf+ncps;)
        *np++ = *sp++;
    csym = rp;
    return(NAME);
}

Questo è un codice che è stato diffuso in samizdat come un'istruzione di buon stile, eppure non lo scriveremo mai così oggi. Guarda quanti effetti collaterali sono stati impacchettati in condizioni di if e while , e viceversa, come entrambi i cicli di for non abbiano un'espressione di incremento perché ciò avviene nel corpo del ciclo. Guarda come i blocchi sono solo avvolti in parentesi graffe quando assolutamente necessario.

Parte di questo è stata certamente la micro-ottimizzazione manuale del tipo che ci aspettiamo che il compilatore faccia per noi oggi, ma vorrei anche proporre l'ipotesi che quando l'intera interfaccia al computer è un singolo tty di vetro 80x25, vuoi il tuo codice deve essere il più denso possibile in modo che tu possa vederne di più allo stesso tempo.

(L'uso di buffer globali per tutto è probabilmente una sbornia dal tagliare i denti sul linguaggio assembly.)

    
risposta data 25.09.2016 - 18:45
fonte
-1

Forse dovresti pensare anche ai cosmetici.

while( i++ )

probabilmente "sembra migliore" di

while( i+=1 )

Per qualche motivo, l'operatore post-incremento sembra molto interessante. È breve, è dolce e aumenta di 1. Confronta con il prefisso:

while( ++i )

"guarda all'indietro" non è vero? Non vedi mai un segno più INIZIALE in matematica, tranne quando qualcuno è stupido nel specificare qualcosa di positivo ( +0 o qualcosa del genere). Siamo abituati a vedere OBJECT + SOMETHING

È qualcosa che ha a che fare con la valutazione? A, A +, A ++? Posso dirvi che all'inizio mi è sembrato piuttosto carino che il popolo di Python abbia rimosso operator++ dalla loro lingua!

    
risposta data 28.12.2011 - 14:21
fonte
-2

Conteggio indietro da 9 a 0 incluso:

for (unsigned int i=10; i-- > 0;)  {
    cout << i << " ";
}

O il ciclo while equivalente.

    
risposta data 01.02.2011 - 15:47
fonte

Leggi altre domande sui tag