2013-04-29 1 views
20

Nella mia app avevo codice simile al seguente. Ho avuto qualche feedback su un bug, quando per il mio horror, ho messo un debugger su di esso e ho scoperto che il MAX tra -5 e 0 è -5!Funzione MAX/MIN nell'obiettivo C che evita i problemi di trasmissione

NSString *test = @"short"; 
int calFailed = MAX(test.length - 10, 0);      // returns -5 

Dopo aver esaminato la macro MAX, vedo che richiede che entrambi i parametri siano dello stesso tipo. Nel mio caso, "test.length" è un int unsigned e 0 è un int firmato. Quindi un cast semplice (per entrambi i parametri) risolve il problema.

NSString *test = @"short"; 
int calExpected = MAX((int)test.length - 10, 0);     // returns 0 

Questo sembra un effetto collaterale sgradevole e inatteso di questa macro. Esiste un altro metodo integrato per iOS per l'esecuzione di MIN/MAX in cui il compilatore avrebbe messo in guardia sui tipi non corrispondenti? Sembra che questo DOVREBBE sia stato un problema in fase di compilazione e non qualcosa che ha richiesto un debugger da capire. Posso sempre scrivere il mio, ma volevo vedere se qualcun altro aveva problemi simili.

+0

Intendi come 'fmax'? –

+0

Grazie. Mi ha aiutato – Raja

risposta

12

Probabilmente non hai abbastanza avvisi del compilatore attivati. Se si attiva -Wsign-compare (che può essere attivata con -Wextra) si genererà un messaggio di avviso che è simile al seguente

warning: signed and unsigned type in conditional expression [-Wsign-compare] 

Questo consente di posizionare i calchi nei posti giusti, se necessario, e non dovrebbe essere necessario per riscrivere le macro MAX o MIN

+3

Sì, questo ha fatto il trucco. Certo, ora il mio progetto ha 107 nuovi numeri. Mi chiedo quanti sono bug come questo. Grazie per il consiglio. – raider33

+1

@ raider33 Vorrei anche aggiungere '-Wall' per attivare più avvisi del compilatore se non lo hai già – FDinoff

+1

Dato che stiamo parlando di ObjC, probabilmente useremo' clang' invece di 'gcc'. Quindi puoi fare meglio di ['-Wall'] (http://assets.diylol.com/hfs/b1e/58b/811/resized/sad-all-the-things-meme-generator-turn-on-all -the-warnings-6e57ad.jpg): attiva ['-Weverything'] (http://assets.diylol.com/hfs/3a0/292/5f5/resized/all-the-things-meme-generator-turn -on-all-the-warnings-5a92fa.jpg) per * tutti gli avvertimenti *. Quindi prova '-Werror' per il raggiungimento della modalità Difficile. – rickster

29

Abilitare -Wsign-compare, come suggerito dalla risposta di FDinoff è una buona idea, ma ho pensato che valesse la pena di spiegare il motivo alla base di alcuni dettagli, poiché è una trappola abbastanza comune.

Il problema non è in realtà con la macro MAX in particolare, ma con a) sottraendo da un numero intero senza segno in un modo che porta a un overflow, e b) (come suggerisce l'avviso) con come il compilatore gestisce il confronto di valori firmati e non firmati in generale.

Il primo problema è abbastanza semplice da spiegare: quando si sottraggono da un numero intero senza segno e il risultato sarebbe negativo, il risultato "trabocca" su un valore positivo molto grande, poiché un numero intero senza segno non può rappresentare valori negativi. Quindi [@"short" length] - 10 valuterà a 4294967291.

Quale potrebbe essere più sorprendente è che, anche senza la sottrazione, qualcosa come MAX([@"short" length], -10) non produrrà il risultato corretto (sarebbe valutare a -10, anche se [@"short" length] sarebbe 5, che è ovviamente più grande). Questo non ha nulla a che fare con la macro, qualcosa come if ([@"short" length] > -10) { ... } porterebbe allo stesso problema (il codice nel if-block sarebbe non eseguito).

Quindi la domanda generale è: cosa succede esattamente quando confronti un intero senza segno con uno firmato (e perché c'è un avviso per quello in primo luogo)? Il compilatore convertirà entrambi i valori in un tipo comune, in base a determinate regole che possono portare a risultati sorprendenti.

Citando Understand integer conversion rules [cert.org]:

  • Se il tipo dell'operando di tipo intero con segno può rappresentare tutti i valori del tipo dell'operando di tipo intero senza segno, l'operando con il tipo intero senza segno è convertito nel tipo di operando con tipo intero con segno.
  • In caso contrario, entrambi gli operandi vengono convertiti nel tipo intero senza segno corrispondente al tipo di operando con tipo intero con segno.

(sottolineatura mia)

Considerate questo esempio:

int s = -1; 
unsigned int u = 1; 
NSLog(@"%i", s < u); 
// -> 0 

Il risultato sarà 0 (false), anche se s (-1) è chiaramente inferiore a u (1). Ciò si verifica perché entrambi i valori vengono convertiti in unsigned int, in quanto int non può rappresentare tutti i valori che possono essere contenuti in un unsigned int.

Diventa ancora più confuso se si modifica il tipo di s in long. Quindi, otterresti lo stesso risultato (errato) su una piattaforma a 32 bit (iOS), ma in un'applicazione Mac a 64 bit funzionerebbe benissimo! (spiegazione: long è lì a 64 bit, quindi può rappresentare tutti i 32 bit unsigned int valori.)

Quindi, per farla breve: non confrontare interi non firmati e con segno, soprattutto se il valore firmato è potenzialmente negativo.

+1

Ottima spiegazione. –