Come gestire l’inizializzazione del membro di riferimento non const in object const?

Diciamo che hai una lezione

class C { int * i; public: C(int * v):i(v) {}; void method() const; //this method does not change i void method(); //this method changes i } 

Ora potresti voler definire l’istanza const di questa class

  const int * k = whatever; const C c1(k); //this will fail 

ma questo fallirà a causa del costruttore C non int const C (int * v)

quindi definisci un costruttore const int

  C(const int * v):i(v) {}; //this will fail also 

Ma questo fallirà anche dal momento che il membro di C “int * i” non è const.

Cosa fare in questi casi? Usa mutabile? Casting? Preparare la versione const della class?

modifica: dopo aver discusso con Pavel (sotto) ho studiato un po ‘questo problema. Per me quello che fa C ++ non è corretto. L’objective del puntatore deve essere di tipo rigoroso, il che significa che non è ansible ad esempio eseguire quanto segue:

 int i; const int * ptr; ptr = & i; 

In questo caso la grammatica tratta la costante come una promise di non cambiare il bersaglio del puntatore. Inoltre, int * const ptr è una promise di non modificare il valore del puntatore stesso. Quindi hai due posti dove può essere applicato const. Quindi si consiglia alla class di modellare un puntatore (perché no). E qui le cose stanno cadendo a pezzi. La grammatica C ++ fornisce metodi const che sono in grado di promettere di non modificare i valori del campo stesso, ma non esiste una grammatica per sottolineare che il tuo metodo non cambierà i target dei tuoi puntatori in-class.

Una soluzione alternativa consiste nel definire due classi const_C e C ad esempio. Tuttavia, non è una strada regale. Con i modelli, le loro specializzazioni parziali è difficile non rimanere confusi. Anche tutte le possibili varianti di argomenti come const const_C & arg , const C & arg , const_C & arg , C & arg non sembrano belle. Io davvero non so cosa fare. Utilizza classi separate o const_casts, ogni modo sembra essere sbagliato.

In entrambi i casi dovrei contrassegnare i metodi che non modificano la destinazione del puntatore come const? O semplicemente seguire il percorso tradizionale che il metodo const non modifica lo stato dell’object stesso (il metodo const non interessa il target del puntatore). Quindi nel mio caso tutti i metodi sarebbero const, perché la class sta modellando un puntatore, quindi il puntatore stesso è T * const . Ma chiaramente alcuni di essi modificano l’objective del puntatore e altri no.

Il tuo esempio non fallisce, k è passato per valore. Il membro i è “implicitamente costante” in quanto i membri diretti di C non possono essere modificati quando l’istanza è costante.
Constness dice che non è ansible cambiare i membri dopo l’inizializzazione, ma è naturalmente consentito inizializzarli con i valori nell’elenco di inizializzazione – in quale altro modo si darebbe loro un valore?

Ciò che non funziona è invocare il costruttore senza renderlo pubblico;)

aggiorna la domanda aggiornata di indirizzamento:

Sì, il linguaggio C ++ ti costringe a volte in qualche parola, ma la correttezza costante è un comportamento standard comune che non puoi ridefinire semplicemente senza infrangere le aspettative. La risposta di Pavels spiega già un idioma comune, che viene utilizzato in librerie comprovate come la STL, per aggirare questa situazione.

A volte devi solo accettare che le lingue hanno dei limiti e continuare a gestire le aspettative degli utenti dell’interfaccia, anche se ciò significa applicare una soluzione apparentemente non ottimale.

Sembra che tu voglia un object che può racchiudere int* (e quindi comportarsi come non-const), o int const* (e quindi comportarsi come const). Non puoi davvero farlo correttamente con una singola class.

In effetti, la nozione che const applicato alla tua class dovrebbe cambiare la sua semantica come se fosse sbagliata – se la tua class modella un puntatore o un iteratore (se avvolge un puntatore, è probabile che sia il caso), quindi const applica a esso dovrebbe significare solo che non può essere cambiato da solo, e non dovrebbe implicare nulla per quanto riguarda il valore indicato. Dovresti considerare di seguire cosa fa STL per i suoi contenitori: è proprio il motivo per cui ha classi iterator e const_iterator distinte, essendo entrambe distinte, ma la prima è implicitamente convertibile in quest’ultima. Inoltre, in AWL, const iterator non è lo stesso di const_iterator ! Quindi fai lo stesso.

[EDIT] Ecco un modo complicato per riutilizzare il codice tra C e const_C al massimo, assicurando al contempo la correttezza della const_C e non approfondendo in UB (con const_cast ):

 template struct pointer_to_maybe_const; template struct pointer_to_maybe_const { typedef const T* type; }; template struct pointer_to_maybe_const { typedef T* type; }; template struct C_fields { typename pointer_to_maybe_const::type i; // repeat for all fields }; template class const_C_base { public: int method() const { // non-mutating method example return *self().i; } private: const Derived& self() const { return *static_cast(this); } }; template class C_base : public const_C_base { public: int method() { // mutating method example return ++*self().i; } private: Derived& self() { return *static_cast(this); } }; class const_C : public const_C_base, private C_fields { friend class const_C_base; }; class C : public C_base, private C_fields { friend class C_base; }; 

Se in realtà hai pochi campi, potrebbe essere più facile duplicarli in entrambe le classi piuttosto che andare in una struttura. Se ce ne sono molti, ma sono tutti dello stesso tipo, è più semplice passare quel tipo come parametro di tipo direttamente e non preoccuparsi del modello di const wra.

La tua domanda non ha senso. Dove hai ottenuto tutte queste previsioni “questo fallirà”? Nessuno di loro è nemmeno lontanamente vero.

In primo luogo, è del tutto irrilevante che il parametro del costruttore sia dichiarato const o no. Quando passi per valore (come nel tuo caso) puoi passare un object const come argomento in ogni caso, indipendentemente dal fatto che il parametro sia dichiarato come const o no.

In secondo luogo, dal punto di vista del costruttore, l’object NON è costante. Indipendentemente dal tipo di object che si sta costruendo (costante o meno), dall’interno del costruttore l’object non è mai costante. Quindi non c’è bisogno di mutable o altro.

Perché non provi semplicemente a compilare il tuo codice (per vedere che nulla fallirà), invece di fare previsioni strane senza fondo che qualcosa “fallirà”?

Un const int * non è lo stesso di un int * const. Quando la tua class è const, hai quest’ultimo (puntatore costante al numero intero mutabile). Quello che stai passando è il primo (puntatore mutabile a numero intero costante). I due non sono intercambiabili, per ovvi motivi.

Quando si istanziare

 const C c1(...) 

Poiché c1 è const, il suo membro si rivolge a:

 int* const i; 

Come menzionato da qualcun altro, questo è chiamato implicit const.

Ora, più avanti nel tuo esempio, si tenta di passare a const int *. Quindi il tuo costruttore sta fondamentalmente facendo questo:

 const int* whatever = ...; int* const i = whatever; // error 

Il motivo per cui ottieni un errore è perché non puoi trasmettere const a non-const. Il puntatore “qualunque” non è autorizzato a cambiare la cosa a cui punta (la parte int è const). Il puntatore “i” può cambiare ciò a cui punta, ma non può essere modificato (la parte puntatore è const).

Si cita anche il volere che la class modellino un puntatore. Lo STL lo fa con gli iteratori. Il modello utilizzato da alcune implementazioni sta avendo una class chiamata ‘const_iterator’ che nasconde il puntatore reale e fornisce solo i metodi const per accedere ai dati puntati. Poi c’è anche una class ‘iterator’ che eredita da ‘const_iterator’, aggiungendo sovraccarichi non costanti. Funziona bene – è una class personalizzata che consente la stessa costanza di puntatori, in cui i tipi puntano i puntatori allo specchio in questo modo:

  • iteratore -> T *
  • const iterator -> T * const
  • const_iterator -> const T *
  • const const_iterator -> const T * const

Spero che abbia senso 🙂

OK ecco quello che ho fatto finora. Per consentire l’ereditarietà dopo la versione const della class senza const_casts o overhead di spazio aggiuntivo ho creato un’unione che assomiglia a quella di:

 template  union MutatedPtr { protected: const T * const_ptr; T * ptr; public: /** * Conversion constructor. * @param ptr pointer. */ MutatedPtr(const T * ptr): const_ptr(ptr) {}; /** * Conversion to T *. */ operator T *() {return ptr;} /** * Conversion to const T *. */ operator const T *() const {return const_ptr;} }; 

Quando il campo MutatedPtr viene dichiarato, finisce in modo che nei metodi const viene restituito const_ptr, mentre quelli non const ottengono plain ptr. Delega la costanza del metodo al puntatore target che ha senso nel mio caso.

Qualche commento?

A proposito, puoi ovviamente fare cose simili con i tipi non puntatori o anche con i metodi, quindi sembra che l’introduzione della parola chiave mutable non sia stata necessaria (?)

Ho incontrato lo stesso sfortunato problema e dopo aver lamentato la mancanza di un costruttore di const in C ++ sono giunto alla conclusione che due templatizzazioni sono le migliori, almeno in termini di riuso.

Una versione molto semplificata del mio caso / soluzione è:

  template< typename DataPtrT > struct BaseImage { BaseImage( const DataPtrT & data ) : m_data( data ) {} DataPtrT getData() { return m_data; } // notice that if DataPtrT is const // internally, this will return // the same const type DataPtrT m_data; }; template< typename DataPtrT > struct DerivedImage : public BaseImage { }; 

C’è una sfortunata perdita di ereditarietà della class, ma nel mio caso era accettabile fare una sorta di operatore di casting per poter eseguire il cast tra i tipi const e non-const con una conoscenza esplicita di come eseguire la conversione sotto il cofano. Ciò combinato con un uso appropriato dei costruttori di copie e / o dell’operatore di dereferenziamento sovraccarico potrebbe portarti dove vuoi essere.

  template< typename OutTypeT, typename inTypeT ) image_cast< shared_ptr >( const shared_ptr & inImage ) { return shared_ptr( new OutTypeT( inImage->getData() ) ); }