size_t o int per dimensioni, indice, ecc


15

In C ++, size_t(o, più correttamente, T::size_typeche è "di solito" size_t; cioè, un unsignedtipo) viene usato come valore di ritorno per size(), l'argomento a operator[], ecc. (Vedi std::vector, et. Al.)

D'altra parte, i linguaggi .NET usano int(e, facoltativamente, long) per lo stesso scopo; infatti, non sono richiesti linguaggi conformi a CLS per supportare tipi non firmati .

Dato che .NET è più recente di C ++, qualcosa mi dice che potrebbero esserci problemi nell'utilizzo unsigned intanche per cose che "non possono" essere negative come un indice o una lunghezza di array. L'approccio C ++ è "artefatto storico" per la compatibilità con le versioni precedenti? O ci sono compromessi di progettazione reali e significativi tra i due approcci?

Perché è importante? Bene ... cosa dovrei usare per una nuova classe multidimensionale in C ++; size_to int?

struct Foo final // e.g., image, matrix, etc.
{
    typedef int32_t /* or int64_t*/ dimension_type; // *OR* always "size_t" ?
    typedef size_t size_type; // c.f., std::vector<>

    dimension_type bar_; // maybe rows, or x
    dimension_type baz_; // e.g., columns, or y

    size_type size() const { ... } // STL-like interface
};

6
Vale la pena notare: in diversi punti di .NET Framework, -1viene restituito da funzioni che restituiscono un indice, per indicare "non trovato" o "fuori portata". Viene inoltre restituito da Compare()funzioni (implementazione IComparable). Un int a 32 bit è considerato il tipo da usare per un numero generale, per quello che spero siano ovvi motivi.
Robert Harvey,

Risposte:


9

Dato che .NET è più recente di C ++, qualcosa mi dice che potrebbero esserci problemi nell'uso di unsigned int anche per cose che "non possono" essere negative come un indice o una lunghezza di array.

Sì. Per alcuni tipi di applicazioni come l'elaborazione di immagini o l'elaborazione di array, è spesso necessario accedere agli elementi relativi alla posizione corrente:

sum = data[k - 2] + data[k - 1] + data[k] + data[k + 1] + ...

In questi tipi di applicazioni, non è possibile eseguire il controllo dell'intervallo con numeri interi senza segno senza pensare attentamente:

if (k - 2 < 0) {
    throw std::out_of_range("will never be thrown"); 
}

if (k < 2) {
    throw std::out_of_range("will be thrown"); 
}

if (k < 2uL) {
    throw std::out_of_range("will be thrown, without signedness ambiguity"); 
}

Invece devi riorganizzare l'espressione di controllo della portata. Questa è la differenza principale. I programmatori devono anche ricordare le regole di conversione dei numeri interi. In caso di dubbi, rileggere http://en.cppreference.com/w/cpp/language/operator_arithmetic#Conversions

Molte applicazioni non devono utilizzare indici di array molto grandi, ma devono eseguire controlli di intervallo. Inoltre, molti programmatori non sono addestrati a fare questa ginnastica di riarrangiamento di espressione. Una singola occasione mancata apre le porte a un exploit.

C # è infatti progettato per quelle applicazioni che non avranno bisogno di più di 2 ^ 31 elementi per array. Ad esempio, un'applicazione per fogli di calcolo non ha bisogno di occuparsi di tante righe, colonne o celle. C # si occupa del limite superiore avendo un'aritmetica controllata opzionale che può essere abilitata per un blocco di codice con una parola chiave senza fare confusione con le opzioni del compilatore. Per questo motivo, C # favorisce l'uso di numeri interi con segno. Quando queste decisioni vengono considerate del tutto, ha senso.

Il C ++ è semplicemente diverso ed è più difficile ottenere il codice corretto.

Per quanto riguarda l'importanza pratica di consentire all'aritmetica firmata di rimuovere una potenziale violazione del "principio del minimo stupore", un caso emblematico è OpenCV, che utilizza numeri interi a 32 bit con segno per l'indice degli elementi della matrice, le dimensioni dell'array, il conteggio dei canali pixel, ecc. Immagine l'elaborazione è un esempio di dominio di programmazione che utilizza pesantemente l'indice di array relativo. Underflow intero senza segno (risultato negativo racchiuso) complicherà notevolmente l'implementazione dell'algoritmo.


Questa è esattamente la mia situazione; grazie per gli esempi specifici. (Sì, lo so, ma può essere utile avere "autorità superiori" da citare.)
Ð

1
@Dan: se hai bisogno di citare qualcosa, questo post sarebbe meglio.
rwong,

1
@Dan: John Regehr sta attivamente ricercando questo problema nei linguaggi di programmazione. Vedi blog.regehr.org/archives/1401
rwong

Ci sono opinioni contrarian: gustedt.wordpress.com/2013/07/15/…
rwong

14

Questa risposta dipende veramente da chi userà il tuo codice e da quali standard vogliono vedere.

size_t è una dimensione intera con uno scopo:

Il tipo size_tè un tipo intero senza segno definito dall'implementazione che è abbastanza grande da contenere la dimensione in byte di qualsiasi oggetto. (Specifica C ++ 11 18.2.6)

Pertanto, ogni volta che si desidera lavorare con la dimensione degli oggetti in byte, è necessario utilizzare size_t. Ora in molti casi, non stai usando queste dimensioni / indici per contare i byte, ma la maggior parte degli sviluppatori sceglie di usarli size_tper coerenza.

Nota che dovresti sempre usare size_tse la tua classe ha lo scopo di avere l'aspetto di una classe STL. Tutte le classi STL nella specifica utilizzano size_t. E 'valido per il compilatore di typedef size_tper essere unsigned int, ed è valida anche per essere typedefed a unsigned long. Se lo usi into longdirettamente, alla fine ti imbatterai in compilatori in cui una persona che pensa che la tua classe abbia seguito lo stile dell'STL viene intrappolata perché non hai seguito lo standard.

Per quanto riguarda l'utilizzo dei tipi firmati, ci sono alcuni vantaggi:

  • Nomi più brevi: è davvero facile per le persone digitare int, ma è molto più difficile ingombrare il codice unsigned int.
  • Un numero intero per ogni dimensione: esiste solo un numero intero conforme a CLS di 32 bit, ovvero Int32. In C ++, ci sono due ( int32_te uint32_t). Ciò può semplificare l'interoperabilità delle API

Il grande svantaggio dei tipi firmati è quello ovvio: perdi metà del tuo dominio. Un numero con segno non può contare fino a un numero senza segno. Quando è arrivato C / C ++, questo è stato molto importante. Uno doveva essere in grado di affrontare la piena capacità del processore e per farlo era necessario utilizzare numeri senza segno.

Per i tipi di applicazioni destinate a .NET, non c'era la necessità di un indice senza segno a dominio completo. Molti degli scopi di tali numeri non sono semplicemente validi in una lingua gestita (viene in mente il pool di memoria). Inoltre, con l'uscita di .NET, i computer a 64 bit rappresentavano chiaramente il futuro. Siamo molto lontani dalla necessità dell'intera gamma di un numero intero a 64 bit, quindi sacrificare un bit non è così doloroso come prima. Se hai davvero bisogno di 4 miliardi di indici, passa semplicemente all'uso di numeri interi a 64 bit. Nel peggiore dei casi, lo esegui su una macchina a 32 bit ed è un po 'lento.

Considero il commercio come uno di convenienza. Se hai abbastanza potenza di calcolo che non ti dispiace sprecare un po 'del tuo tipo di indice che non userai mai e poi mai, allora è conveniente semplicemente digitare into longe andarsene. Se scopri di volere davvero quell'ultimo pezzo, probabilmente dovresti prestare attenzione alla firma dei tuoi numeri.


diciamo che l'implementazione di size()era return bar_ * baz_;; questo non crea ora un potenziale problema con overflow di interi (avvolgente) che non avrei se non lo avessi usato size_t?
Ðаn

5
@Dan Puoi costruire casi come quelli in cui contano gli integri non firmati, e in quei casi è meglio usare le funzionalità del linguaggio completo per risolverlo. Tuttavia, devo dire che sarebbe una costruzione interessante avere una classe in cui bar_ * baz_può traboccare un numero intero con segno ma non un numero intero senza segno. Limitandoci al C ++, vale la pena notare che l'overflow senza segno è definito nelle specifiche, ma l'overflow con segno è un comportamento indefinito, quindi se l'aritmetica del modulo di numeri interi senza segno è desiderabile, usali sicuramente, perché è effettivamente definita!
Cort Ammon - Ripristina Monica il

1
@Dan - se la moltiplicazione firmata hasize() traboccato , sei nella lingua UB land. (e in modalità, vedi il prossimo :) Quando poi , con solo un pochino di più, ha traboccato la moltiplicazione senza segno , tu nella terra del bug del codice utente, restituiresti una dimensione fasulla. Quindi non credo che gli unsigned acquistino molto qui. fwrapv
Martin Ba

4

Penso che la risposta del rwong sopra evidenzi già in modo eccellente i problemi.

Aggiungerò il mio 002:

  • size_t, cioè una dimensione che ...

    può memorizzare la dimensione massima di un oggetto teoricamente possibile di qualsiasi tipo (compreso l'array).

    ... è richiesto solo per gli indici di intervallo quando sizeof(type)==1, cioè, se hai a che fare con i chartipi byte ( ). (Ma, notiamo, può essere più piccolo di un tipo ptr :

  • Come tale, xxx::size_typepotrebbe essere utilizzato nel 99,9% dei casi anche se fosse un tipo con dimensioni firmate. (confronta ssize_t)
  • Il fatto che gli std::vectoramici abbiano scelto size_t, un tipo senza segno , per le dimensioni e l'indicizzazione è considerato da alcuni un difetto di progettazione. Concordo. (Seriamente, prenditi 5 minuti e guarda i fulmini di CppCon 2016: Jon Kalb “unsigned: A Guideline for Better Code" .)
  • Quando si progetta un'API C ++ oggi, ci si trova in una posizione stretta: utilizzare size_tper essere coerenti con la libreria standard o utilizzare (un segno ) intptr_to ssize_tper calcoli di indicizzazione facili e meno soggetti a bug.
  • Non utilizzare int32 o int64: utilizzare intptr_tse si desidera andare firmati e si desidera la dimensione della parola macchina o utilizzare ssize_t.

Per rispondere direttamente alla domanda, non è del tutto un "manufatto storico", in quanto il problema teorico della necessità di affrontare più della metà dello spazio di "indicizzazione", o) deve essere, aehm, affrontato in qualche modo in un linguaggio di basso livello come C ++.

Con il senno di poi, penso, personalmente , è un difetto di progettazione che la Libreria standard utilizza senza segno size_tovunque, anche se non rappresenta una dimensione di memoria grezza, ma una capacità di dati digitati, come per le raccolte:

  • date le regole di promozione dei numeri interi di C ++ ->
  • i tipi senza segno semplicemente non sono buoni candidati per tipi "semantici" per qualcosa come una dimensione che è semanticamente non firmata.

Ripeto i consigli di Jon qui:

  • Seleziona i tipi per le operazioni che supportano (non l'intervallo di valori). (* 1)
  • Non utilizzare tipi non firmati nell'API. Questo nasconde bug senza alcun vantaggio al rialzo.
  • Non utilizzare "unsigned" per le quantità. (* 2)

(* 1) cioè unsigned == maschera di bit, non fare mai matematica su di essa (qui arriva la prima eccezione - potresti aver bisogno di un contatore che avvolge - questo deve essere un tipo senza segno.)

(* 2) quantità che significano qualcosa su cui conti e / o fai matematica.


Che cosa intendi con "memoria flat avilable completa"? Inoltre, sicuro di non volerlo ssize_t, definito come pendente firmato size_tinvece di intptr_t, che può memorizzare qualsiasi puntatore (non membro) e potrebbe quindi essere più grande?
Deduplicatore

@Deduplicator - Beh, immagino che la size_tdefinizione sia leggermente incasinata. Vedi size_t vs. intptr ed en.cppreference.com/w/cpp/types/size_t Oggi ho imparato qualcosa di nuovo. :-) Penso che il resto degli argomenti rimanga, vedrò se riesco a correggere i tipi utilizzati.
Martin Ba,

0

Aggiungerò solo che per motivi di prestazioni normalmente utilizzo size_t, per garantire che i calcoli errati causino un underflow che significa che entrambi i controlli di intervallo (sotto zero e sopra size ()) possono essere ridotti a uno:

usando int firmato:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

if (i < 0)
{
    //error
}

if (i > size())
{
    //error
}

usando int non firmato:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

/// This will underflow any number below zero, so that it becomes a very big *positive* number instead.
uint32_t asUnsigned = static_cast<uint32_t>(i);

/// We now don't need to check for below zero, since an unsigned integer can only be positive.
if (asUnsigned > size())
{
    //error
}

1
Vuoi davvero spiegarlo più a fondo.
Martin Ba,

Per rendere la risposta più utile, forse puoi descrivere come appaiono i limiti di array interi o il confronto offset (con o senza segno) nel codice macchina di vari fornitori di compilatori. Esistono molti compilatori C ++ online e siti di disassemblaggio che possono mostrare il codice macchina compilato corrispondente per il codice C ++ e i flag del compilatore forniti.
rwong,

Ho provato a spiegarlo ancora.
asger
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.