I am looking to do something like this (C#).
public final class ImmutableClass {
public readonly int i;
public readonly OtherImmutableClass o;
public readonly ReadOnlyCollection<OtherImmutableClass> r;
public ImmutableClass(int i, OtherImmutableClass o,
ReadOnlyCollection<OtherImmutableClass> r) : i(i), o(o), r(r) {}
}
The potential solutions and their associated problems I've encountered are:
1. Using const
for the class members, but this means the default copy assignment operator is deleted.
Solution 1:
struct OtherImmutableObject {
const int i1;
const int i2;
OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
}
Problem 1:
OtherImmutableObject o1(1,2);
OtherImmutableObject o2(2,3);
o1 = o2; // error: use of deleted function 'OtherImmutableObject& OtherImmutableObject::operator=(const OtherImmutableObject&)`
EDIT: This is important as I would like to store immutable objects in a std::vector
but receive error: use of deleted function 'OtherImmutableObject& OtherImmutableObject::operator=(OtherImmutableObject&&)
2. Using get methods and returning values, but this means that large objects would have to be copied which is an inefficiency I'd like to know how to avoid. This thread suggests the get solution, but it doesn't address how to handle passing non-primitive objects without copying the original object.
Solution 2:
class OtherImmutableObject {
int i1;
int i2;
public:
OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
int GetI1() { return i1; }
int GetI2() { return i2; }
}
class ImmutableObject {
int i1;
OtherImmutableObject o;
std::vector<OtherImmutableObject> v;
public:
ImmutableObject(int i1, OtherImmutableObject o,
std::vector<OtherImmutableObject> v) : i1(i1), o(o), v(v) {}
int GetI1() { return i1; }
OtherImmutableObject GetO() { return o; } // Copies a value that should be immutable and therefore able to be safely used elsewhere.
std::vector<OtherImmutableObject> GetV() { return v; } // Copies the vector.
}
Problem 2: The unnecessary copies are inefficient.
3. Using get methods and returning const
references or const
pointers but this could leave hanging references or pointers. This thread talks about the dangers of references going out of scope from function returns.
Solution 3:
class OtherImmutableObject {
int i1;
int i2;
public:
OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
int GetI1() { return i1; }
int GetI2() { return i2; }
}
class ImmutableObject {
int i1;
OtherImmutableObject o;
std::vector<OtherImmutableObject> v;
public:
ImmutableObject(int i1, OtherImmutableObject o,
std::vector<OtherImmutableObject> v) : i1(i1), o(o), v(v) {}
int GetI1() { return i1; }
const OtherImmutableObject& GetO() { return o; }
const std::vector<OtherImmutableObject>& GetV() { return v; }
}
Problem 3:
ImmutableObject immutable_object(1,o,v);
// elsewhere in code...
OtherImmutableObject& other_immutable_object = immutable_object.GetO();
// Somewhere else immutable_object goes out of scope, but not other_immutable_object
// ...and then...
other_immutable_object.GetI1();
// The previous line is undefined behaviour as immutable_object.o will have been deleted with immutable_object going out of scope
Undefined behaviour can occur due to returning a reference from any of the Get
methods.
You truly want immutable objects of some type plus value semantics (as you care about runtime performance and want to avoid the heap). Just define a struct
with all data members public
.
struct Immutable {
const std::string str;
const int i;
};
You can instantiate and copy them, read data members, but that's about it. Move-constructing an instance from an rvalue reference of another one still copies.
Immutable obj1{"...", 42};
Immutable obj2 = obj1;
Immutable obj3 = std::move(obj1); // Copies, too
obj3 = obj2; // Error, cannot assign
This way, you really make sure every usage of your class respects the immutability (assuming no one does bad const_cast
things). Additional functionality can be provided through free functions, there is no point in adding member functions to a read-only aggregation of data members.
You want 1., still with value semantics, but slightly relaxed (such that the objects aren't really immutable anymore) and you're also concerned that you need move-construction for the sake of runtime performance. There is no way around private
data members and getter member functions:
class Immutable {
public:
Immutable(std::string str, int i) : str{std::move(str)}, i{i} {}
const std::string& getStr() const { return str; }
int getI() const { return i; }
private:
std::string str;
int i;
};
Usage is the same, but the move construction really does move.
Immutable obj1{"...", 42};
Immutable obj2 = obj1;
Immutable obj3 = std::move(obj1); // Ok, does move-construct members
Whether you want assignment to be allowed or not is under your control now. Just = delete
the assignment operators if you don't want it, otherwise go with the compiler-generated one or implement your own.
obj3 = obj2; // Ok if not manually disabled
You don't care about value semantics and/or atomic reference count increments are ok in your scenario. Use the solution depicted in @NathanOliver's answer.