26

Quindi ho un codice funzionale piuttosto esteso in cui il tipo di dati principale è costituito da strutture/classi immutabili. Il modo in cui dichiaro l'immutabilità è "praticamente immutabile" rendendo le variabili membro e tutti i metodi const.Modo idiomatico per dichiarare classi immutabili C++

struct RockSolid { 
    const float x; 
    const float y; 
    float MakeHarderConcrete() const { return x + y; } 
} 

È in questo modo "dovremmo farlo" in C++? O c'è un modo migliore?

+1

Dipende molto dal tuo concetto desiderato di immutabile. Prendi in considerazione le stringhe Java e C#. Sono immutabili ma assegnabili. –

+3

I membri di dati 'const' hanno un vantaggio in quanto si ottiene un errore se si dimentica di inizializzare tale membro di tipo base. Uno svantaggio è che non è possibile assegnare a variabili del tipo. –

+1

Uno svantaggio principale è che non è possibile spostare i dati da un oggetto se i suoi membri sono 'const', motivo per cui preferirei rendere privati ​​i membri e fornire invece getter e metodi' const' invece come suggerisce oikosdev – Joe

risposta

29

Il modo in cui avete proposto è perfettamente bene, a meno che nel codice è necessario effettuare l'assegnazione delle variabili ROCKSOLID, in questo modo:

RockSolid a(0,1); 
RockSolid b(0,1); 
a = b; 

Questo non avrebbe funzionato come l'operatore di assegnamento per copia sarebbe stato cancellato dallo il compilatore.

Quindi un'alternativa è riscrivere la struct come una classe con membri dati privati ​​e solo funzioni const pubbliche.

class RockSolid { 
    private: 
    float x; 
    float y; 

    public: 
    RockSolid(float _x, float _y) : x(_x), y(_y) { 
    } 
    float MakeHarderConcrete() const { return x + y; } 
    float getX() const { return x; } 
    float getY() const { return y; } 
} 

In questo modo, gli oggetti sono ROCKSOLID (pseudo-) immutabili, ma sono ancora in grado di fare le assegnazioni.

+1

La semantica I particolarmente desiderabile, almeno per questo progetto, sono gli stessi di "900 libbre linguaggi funzionali" come Haskell in cui qualsiasi nuovo stato deve essere esplicitamente (tipo) costruito. – BlamKiwi

+2

Puoi prendere in considerazione l'uso del [idioma compagno immutabile valore mutabile] (http://martin-moene.blogspot.com/2012/08/growing-immutable-value.html). –

+2

@MartinMoene: Quell '"idioma" crea ancora oggetti valore non assegnabili alla copia e non trasferibili (o si basa sull'heap). La soluzione di oikosdev non soffre per questo inconveniente e non ha bisogno di una (complice) caldaia. Funziona solo ... Perché complicare tutto? – paercebal

7

Presumo che il tuo obiettivo sia la vera immutabilità - ogni oggetto, una volta costruito, non può essere modificato. Non è possibile assegnare un oggetto a un altro.

Il più grande svantaggio del tuo progetto è che non è compatibile con la semantica del movimento, il che può rendere le funzioni che rendono tali oggetti più pratici.

Per fare un esempio:

struct RockSolidLayers { 
    const std::vector<RockSolid> layers; 
}; 

possiamo creare uno di questi, ma se abbiamo una funzione per crearlo:

RockSolidLayers make_layers(); 

si deve (logicamente) copiare il suo contenuto verso il restituire il valore o utilizzare la sintassi return {} per costruirlo direttamente. All'esterno, devi fare:

RockSolidLayers&& layers = make_layers(); 

o ancora (logicamente) copia-costruisci. L'incapacità di spostare-costruire ostacolerà una serie di semplici modi per avere un codice ottimale.

Ora, sia di quelle di copia-costruzioni sono eliso, ma il caso più generale in possesso - non si può spostare i dati da un oggetto chiamato ad un altro, come C++ non ha un'operazione di "distruggere e spostare" che entrambi prendono una variabile fuori dall'ambito e la usano per costruire qualcos'altro.

E i casi in cui C++ sposta implicitamente l'oggetto (return local_variable; per esempio) prima della distruzione vengono bloccati dai membri di dati const.

In un linguaggio progettato intorno a dati immutabili, sarebbe in grado di "spostare" i dati nonostante la sua (logica) immutabilità.

Un modo per risolvere questo problema consiste nell'utilizzare l'heap e archiviare i dati in std::shared_ptr<const Foo>. Ora il numero const non è nei dati dei membri, ma piuttosto nella variabile. Puoi anche esporre solo le funzioni di fabbrica per ognuno dei tuoi tipi che restituisce il precedente shared_ptr<const Foo>, bloccando altre costruzioni.

Tali oggetti possono essere composti, con Bar memorizzando membri std::shared_ptr<const Foo>.

Una funzione che restituisce un std::shared_ptr<const X> può spostare in modo efficiente i dati e una variabile locale può avere il suo stato spostato in un'altra funzione una volta che hai finito senza poter fare confusione con i dati "reali".

Per una tecnica correlata, è idomatic in C++ meno vincolato a prendere tale shared_ptr<const X> e memorizzarli in un tipo di wrapper che finge di non essere immutabile. Quando si esegue un'operaiton mutante, lo shared_ptr<const X> viene clonato e modificato, quindi memorizzato. Un'ottimizzazione "sa" che lo shared_ptr<const X> è "veramente" un (nota: assicurarsi che le funzioni di fabbrica restituiscano un cast a shared_ptr<const X> o che questo non sia effettivamente vero), e quando lo use_count() è 1 invece getta via const e lo modifica direttamente . Questa è un'implementazione della tecnica nota come "copia su scrittura".

+1

o 'std :: unique_ptr ', che può essere restituito dal valore tramite 'std :: move()'. – Vorac

Problemi correlati