Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Idiomatic way to create an immutable and efficient class in C++

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.

like image 930
lachy Avatar asked Aug 29 '19 12:08

lachy


People also ask

Are immutable objects more efficient?

Strings immutability makes it possible to share them, this is more efficient memory-wise.


1 Answers

  1. 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.

  2. 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 
  3. 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.

like image 130
lubgr Avatar answered Oct 02 '22 15:10

lubgr