Struttura immutabile in C ++

1

Mi piacerebbe essere in grado di implementare dati immutabili in C ++. In breve, dato un oggetto C ++ in cui vorrei modificare una variabile membro, invece di modificare quel membro sul posto vorrei ottenere una nuova copia dell'oggetto con il membro modificato.

Per risolvere questo problema, ho scritto una piccola classe template per rappresentare il campo immutabile:

template <typename ParentStruct, typename T>
class ImmutableField {
public:

  ParentStruct set(const T& newNalue) const {
    ParentStruct dst = *(getParentPointer());
    reinterpret_cast<ImmutableField<ParentStruct, T>*>((
        reinterpret_cast<uint8_t*>(&dst) + _offsetInParent))->_value = newNalue;
    return dst;
  }

  const T& get() const {
    return _value;
  }

private:
  friend ParentStruct;

  ImmutableField(const ParentStruct* parent, const T& init = T()) : 
    _offsetInParent(reinterpret_cast<const uint8_t*>(this) - reinterpret_cast<const uint8_t*>(parent)),
    _value(init) {}

  const ParentStruct* getParentPointer() const {
    return reinterpret_cast<const ParentStruct*>(
      reinterpret_cast<const uint8_t*>(this) - _offsetInParent);
  }

  T _value;
  int64_t _offsetInParent = 0;
};

Come parametro template, prende il tipo della classe a cui appartiene e il tipo di valore che memorizza. Ha due metodi pubblici, get e set . Tieni presente che set è const e restituisce una ParentStruct che rappresenta la versione aggiornata di ParentStruct.

Ora posso, ad esempio, usarlo per implementare un numero complesso come questo:

double sqr(double x) {return x*x;}

struct ComplexNumber {
  ComplexNumber() : real(this), imag(this) {}

  ImmutableField<ComplexNumber, double> real;
  ImmutableField<ComplexNumber, double> imag;

  double abs() const {
    return sqrt(sqr(real.get()) + sqr(imag.get()));
  }
};

E alcuni test di base suggeriscono che funzioni:

#define CHECK(X) if (!(X)) {std::cerr << "Check " #X " failed." << std::endl; abort();}

int main() {

  ComplexNumber x = ComplexNumber().real.set(3.0).imag.set(4.0);
  CHECK(x.abs() == 5.0);

  std::cout << "So far, so good!" << std::endl;

  ComplexNumber y = x.real.set(12).imag.set(5);

  CHECK(y.abs() == 13.0);

  std::cout << "It still works!" << std::endl;

  return 0;
}

La mia domanda è : c'è un modo migliore per ottenere ciò che sto cercando di fare e c'è una libreria per farlo in un modo più pulito? La classe ImmutableField sembra estremamente hacky con un sacco di reinterpret_cast .

    
posta Rulle 02.11.2018 - 10:41
fonte

1 risposta

7

Sebbene l'obiettivo dell'utilizzo di tipi immutabili sia di solito lodevole, l'implementazione specifica è eccessivamente intelligente, comporta un sacco di UB e fallirà per qualsiasi ParentStruct che non sia banalmente copiabile . Inoltre, le istanze del campo sono associate a un'istanza specifica. Poiché questi campi non devono essere copiati, è necessario eliminare almeno i costruttori di copia e gli operatori di assegnazione. Nota inoltre che i tuoi campi hanno un overhead comparativamente enorme di int64_t - non sono un'astrazione a costo zero! (Inoltre, probabilmente dovresti usare ptrdiff_t tipi invece di tipi interi espliciti.)

Una parte del tuo problema concettuale è che C ++ è un linguaggio orientato al valore, non un linguaggio orientato al riferimento come Java. In C ++, una variabile è l'oggetto. Una variabile non può essere riassegnata a un oggetto diverso, ma possiamo sovrascrivere il contenuto dell'oggetto con un nuovo valore. Ciò ha un impatto diretto su come pensiamo all'immutabilità. In particolare, avere valori mutabili non è immediatamente problematico. Dove vogliamo disabilitare le modifiche a un valore tramite un riferimento specifico (come un nome di variabile), possiamo contrassegnare tale riferimento const .

C ++ ha già un modo accettabile per creare un nuovo oggetto con un solo campo modificato: crea una copia, quindi cambia quel campo.

#include <iostream>
#include <cmath>

struct ComplexNumber {
  ComplexNumber() : real(), imag() {}
  ComplexNumber(double real, double imag) : real(real), imag(imag) {}

  double real;
  double imag;

  double abs() const {
    return std::sqrt(real*real + imag*imag);
  }
};

#define CHECK(X) if (!(X)) {std::cerr << "Check " #X << " failed." << std::endl; abort();}

int main() {
  const ComplexNumber x = ComplexNumber(3.0, 4.0);
  CHECK(x.abs() == 5.0);

  std::cout << "So far, so good!" << std::endl;

  ComplexNumber y = x;
  y.real = 12;
  y.imag = 5;

  CHECK(y.abs() == 13.0);

  std::cout << "It still works!" << std::endl;

  return 0;
}

Se y deve essere const , allora possiamo usare un lambda immediatamente invocato:

const ComplexNumber y = [&]() {
  ComplexNumber y = x;
  y.real = 12;
  y.imag = 5;
  return y;
}();

(che, grazie a copia elision, non richiede una copia di inner- y alla variabile esterna.)

Questo pattern è anche ben generalizzabile in modo che tu possa scrivere una macro per definire i metodi setter:

#define SETTER(type, name) \
  auto with_##name(type value) const { \
    auto copy = *this; \
    copy.name = value; \
    return copy; \
  }

Quindi: const ComplexNumber y = x.with_real(12).with_imag(5) al costo di una copia extra.

    
risposta data 02.11.2018 - 12:23
fonte

Leggi altre domande sui tag