2013-02-11 21 views
7

Con una classe contenitore, ad esempio std::vector, esistono due diversi concetti di costante: quello del contenitore (ovvero la sua dimensione) e quello degli elementi. Sembra che std::vector confonde questi due, in modo tale che il seguente codice semplice non viene compilato:const vs non-const del contenitore e del suo contenuto

struct A { 
    A(size_t n) : X(n) {} 
    int&x(int i) const { return X[i]; } // error: X[i] is non-const. 
private: 
    std::vector<int> X; 
}; 

Si noti che anche se i membri di dati (i tre puntatori alla iniziano & fine dei dati e alla fine del buffer assegnato) di std::vector sono non modificato da una chiamata al suo operator[], questo membro non è const - non è uno strano design?

noti inoltre che per i puntatori prime, questi due concetti di costante-ness sono ben separati, in modo tale che il codice grezzo puntatore corrispondente

struct B { 
    B(size_t n) : X(new int[n]) {} 
    ~B() { delete[] X; } 
    void resize(size_t n);     // non-const 
    int&x(int i) const { return X[i]; } // fine 
private: 
    int*X; 
}; 

funziona bene.

Quindi qual è il modo corretto/consigliato per gestirlo quando si utilizza std::vector (senza utilizzare mutable)?

È un const_cast<> come in

int&A::x(int i) const { return const_cast<std::vector<int>&>(X)[i]; } 

giudicate accettabili (X è noto per essere non const, quindi non UB qui)?

EDIT proprio per prevenire ulteriore confusione: voglio modificare gli elementi, cioè i contenuto del contenitore ma non il contenitore stesso (la dimensione e/o posizione di memoria).

+0

I dati di 'std :: VECTOR' potrebbe essere cambiato dalla vostra chiamata a' operatore [] '. Come hai scritto 'A :: X',' a.x (1) ++; 'è perfettamente legale e modifica il contenuto del vettore. –

+1

@DavidSchwartz I * contenuti * di un vettore non sono i suoi effettivi * dati * (sebbene sia possibile associarli logicamente come tali). Se ispezionate 'std :: vector', ha solo 3 puntatori come dati (inizio e fine dei dati e fine del buffer). Questi rimangono invariati. – Walter

risposta

13

C++ supporta un solo livello di const. Per quanto riguarda il compilatore è interessato, è bit a bit const: i "bit" in realtà nell'oggetto (cioè contati in sizeof) non possono essere modificate senza giochi di gioco (const_cast, ecc), ma tutto il resto è giusto gioco . Nei primi giorni di C++ (fine del 1980, primi anni 1990) ci stato un sacco di discussioni per quanto riguarda i vantaggi progettuali di bit a bit const vs. const logico (noto anche come Humpty-Dumpty const, perché come Andy Koenig una volta mi ha detto , quando il programmatore usa const, significa esattamente ciò che il programmatore vuole che significhi). Il consenso alla fine si è coalizzato a favore della logica const.

Ciò vuol dire che gli autori di classi contenitore devono fare una scelta. Gli elementi della parte del container del contenitore sono o meno. Se fanno parte del contenitore, non possono essere modificati se il contenitore è const. Non c'è modo di offrire ; l'autore del contenitore deve scegliere uno o l'altro. Anche qui, sembra esserci un consenso: i elementi sono parte del contenitore, e se il contenitore è const, non possono essere modificati. (Forse il parallelo con array stile C ha avuto un ruolo qui; se una matrice stile C è const, allora non si può modificare uno dei suoi elementi.)

Come te, ho volte incontrato quando ho voluto proibire modifica della dimensione del vettore (forse per proteggere gli iteratori ), ma non dei suoi elementi. Non ci sono davvero soluzioni soddisfacenti ; il meglio che posso pensare è creare un nuovo tipo , che contenga un mutable std::vector, e fornire le funzioni di inoltro corrispondenti al significato di const di cui ho bisogno in questo caso specifico. E se vuoi distinguere tre livelli (completamente const, parzialmente const e non-const), avrai bisogno di derivazione. La classe di base espone solo le funzioni completamente const e parzialmente const (ad esempio un const int operator[](size_t index) const; e int operator[]( size_t index);, ma non void push_back(int);); le funzioni che consentono l'inserimento e la rimozione di un elemento sono esposte solo nella classe derivata. I client che non devono inserire o rimuovere elementi passano solo un riferimento non costante alla classe base.

+0

+1 nice discussione. Non sono convinto, tuttavia, del fatto che il progettista di una classe di contenitori non abbia scelta. Si potrebbe implementare una classe contenitore che trasporta la costanza dei suoi elementi come argomento del modello e consentire la conversione del movimento (usando puntatori intelligenti) tra contenitori const e non-const. Tuttavia, il tuo design derivato dalla base sembra più semplice. – Walter

+0

@Walter Il progettista di una classe contenitore ha tutti i tipi di scelte. Il progettista di 'std :: vector' ha fatto, in questo caso, quello per cui sembra esserci un consenso generale --- la maggior parte dei contenitori pre-standard di cui sono a conoscenza hanno fatto lo stesso. Globalmente, non sembra esserci una grande richiesta per un contenitore che offre i tre livelli di 'const' (nessuno, parziale o completo), tranne in casi speciali (ad esempio' std :: array'). (Nei giorni pre-standard, una delle mie classi di array prendeva le dimensioni come argomento costruttore e non offriva alcuna possibilità di cambiarla in seguito.) –

+0

@JamesKanze: la spiegazione è eccellente, tuttavia le prime due frasi di apertura in cima hanno dato l'impressione che const bitwise sia quella supportata da C++. * (Spero che non ci siano troppi programmatori C++ che sono TL; DR.) * Questa frase di apertura è vera sul back-end del compilatore, ma il front-end considera solo 'const' un aiuto sintattico - è usato nel controllo dei tipi, è così. (A proposito, C++ 11 introduce 'constexpr' in fase di compilazione.) La costanza logica esiste solo nei cervelli dei programmatori. Un programmatore C++ realizzerà le differenze quando si inizia la programmazione multi-thread. – rwong

3

Non è un disegno strano, è una scelta molto deliberata e quella giusta IMHO.

tuo B esempio non è una buona analogia per un std::vector, un'analogia migliore sarebbe:

struct C { 
    int& get(int i) const { return X[i]; } 
    int X[N]; 
}; 

ma con la differenza molto utile che l'array può essere ridimensionato. Il codice sopra non è valido per lo stesso motivo del tuo originale, gli elementi dell'array (o vector) sono concettualmente "membri" (tecnicamente sotto-oggetti) del tipo contenente, quindi non dovresti essere in grado di modificarli tramite un membro const funzione.

Direi che lo const_cast non è accettabile e nessuno dei due utilizza mutable a meno che non sia l'ultima risorsa. Dovresti chiedere perché vuoi modificare i dati di un oggetto const e considerare di rendere non-const la funzione membro.

+0

Questa è una questione di interpretazione. Se vedi un 'vector' come un array C che è ridimensionabile, allora sono d'accordo. Tuttavia, allora in che punto le funzioni 'resize()' etc si adattano a questa analogia? Devono essere più che costanti: vuoi essere in grado di dare accesso non const agli elementi ma non alle dimensioni. Questo non è possibile con 'std :: vector'. Penso che il solito modo in cui questo è risolto sia quello di fornire iteratori, che in genere permettono di cambiare il contenuto, ma non il contenitore. – Walter

+0

È solo un'analogia, non prenderla troppo alla lettera, comunque 'resize' non è const, quindi è permesso di mutare l'oggetto. Non sei sicuro di cosa intendi su iteratori, ma nessuno dei contenitori standard ti darà un iteratore non const per un contenitore const. –

+0

ciò che intendevo per gli iteratori è che se si ha un iteratore (non costante) non è possibile modificare il contenitore. Quindi il contenitore rimane const mentre gli elementi possono essere modificati. – Walter

4

Purtroppo, a differenza di puntatori, non si può fare qualcosa di simile

std::vector<int> i; 
std::vector<const int>& ref = i; 

Ecco perché std::vector non può disambiguare tra i due tipi di const quali si applicano, e deve essere conservatori.Io, personalmente, avrei scelto di fare qualcosa di simile

const_cast<int&>(X[i]); 

Edit: Come un altro commentatore ha sottolineato con precisione, iteratori fanno modello di questa dicotomia. Se è stato memorizzato un vector<int>::iterator all'inizio, è possibile de-referenziare in un metodo const e recuperare un valore non costante int&. Credo. Ma dovresti fare attenzione all'invalidazione.

+1

L'uso degli iteratori suggerisce una soluzione interessante: una classe _view_ che contiene solo l'iteratore di inizio e fine e fornisce solo la funzionalità limitata che desidera. (Per questo, anche una classe vista con un puntatore al vettore farebbe lo stesso.) –

-1

Io suggerirei di usare std::vector::at() metodo al posto di un const_cast.

+0

Il sovraccarico const di 'vector :: at()' restituisce un riferimento 'const', quindi non sarà di aiuto –

Problemi correlati