简体   繁体   中英

Does C++ support STL-compatible immutable record types?

A number of languages have support for immutable types – once you have constructed the value of such a type, it cannot be modified in any way. I won't digress into the benefits of such types, as this has been discussed extensively elsewhere. (See eg http://codebetter.com/patricksmacchia/2008/01/13/immutable-types-understand-them-and-use-them/ )

I would like to create a lightweight immutable record type in C++. An obvious way of doing this would be to const all the members. For example:

struct Coordinates {
    const double x;
    const double y;
};

Unfortunately, a type declared in this way doesn't support copy assignment, and as such can't be used with STL containers, which is a pretty fatal drawback. I don't think there is any way around this, but would be grateful to know if anyone can see one.

For what it's worth, as far as I can see, C++ is conflating two things: a) whether you can assign a new value to a variable, and b) whether "values" (=objects) can be modified after they are constructed. The distinction is much clearer in eg Scala, where one has

  • val vs var : a var can have a new value bound to it, a val cannot
  • immutable and mutable objects, particularly collections

So I can write the following:

val val_mutable = collection.mutable.Map(1 -> "One")
val val_immutable = collection.immutable.Map(1 -> "One")
var var_mutable = collection.mutable.Map(1 -> "One")
var var_immutable = collection.immutable.Map(1 -> "One")

var s can be rebound to point at other values:

//FAILS: val_immutable = collection.immutable.Map(2 -> "Two")
//FAILS: val_mutable = collection.mutable.Map(2 -> "Two")
var_mutable = collection.mutable.Map(2 -> "Two")
var_immutable = collection.immutable.Map(2 -> "Two")

mutable collections can be modified:

val_mutable(2) = "Two"    
//FAILS: val_immutable(2) = "Two"    
var_mutable(2) = "Two"    
//FAILS: var_immutable(2) = "Two"    

In C++, const ing the data members of a type makes it an immutable type but also makes it impossible to create "var"s of that type. Can anyone can see a reasonably lightweight way of achieving the former without the latter? [Please note the "lightweight" part -- in particular, I do not want to create an accessor function for every element of the struct , as this results in a massive drop in readability.]

Remember that assignment in most other languages behaves like pointer reassignment in C++.

Thus, a std::shared_ptr<const T> in C++ has similar semantics to immutable types in other languages.

struct Coordinates {
    double x;
    double y;
};
std::shared_ptr<const Coordinates> p = std::make_shared<const Coordinates>(Coordinates{3.14, 42.0});
p->x *= 2;                                           // error; pointee is immutable
p = std::make_shared<const Coordinates>(Coordinates{6.28, 42.0}); // ok

if you want to mimic your var / val , you may create 2 classes similar to:

template <typename T> class val
{
public:
    val(const T& t) : t(t) {}

    val(const val&) = default;
    val& operator = (const val&) = delete;

    const T* operator->() const { return &t; }
    const T& get() const { return t; }

    T* operator->() { return &t; }
    T& get() { return t; }

private:
    T t;
};

template <typename T> class var
{
public:
    var(const T& t) : t(t) {}

    var(const var&) = default;
    var& operator = (const var&) = default;
    var& operator = (const T&) { this->t = t; return *this; };

    const T* operator->() const { return &t; }
    const T& get() const { return t; }

    T* operator->() { return &t; }
    T& get() { return t; }

private:
    std::remove_const_t<T> t;
};

And then

val<const Coordinate> immutable_val({42, 51});
val<Coordinate> mutable_val({42, 51});
var<const Coordinate> immutable_var({42, 51});
var<Coordinate> mutable_var({42, 51});

//immutable_val->x = 42; // error
mutable_val->x = 42;
//immutable_var->x = 42; // error
mutable_var->x = 42;

//immutable_val = {42, 42}; // error
//mutable_val = {42, 42}; // error
immutable_var = {42, 42};
mutable_var = {42, 42};

//mutable_val = mutable_val; // error
//immutable_val = immutable_val; // error
immutable_var = immutable_var; 
mutable_var = mutable_var;

Demo

You mention that you can't use the Coordinates class with STL containers as written with const members. The question of how to do that is answered (inconclusively) here:

https://stackoverflow.com/a/3372966/393816

The same suggestion, which is to make your instances const rather than your members, has been made in comment threads on this question too. I agree with your comment that it is less reliable since you must change more code, but perhaps you could use a typedef like so:

struct MutableCoordinates {
  double x;
  double y;
};

typedef const MutableCoordinates Coordinates;

So that users of Coordinates objects automatically get the immutable version unless they explicitly as for it? This would offset some of your worry about the verbosity of using the struct safely.

Of course, you can also switch to reference semantics by using shared_ptr , which will work very well with const objects since the different references can't mess with each other, but it comes with the overhead of manual memory allocation which is rather excessive for a struct of two double s.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM