0

Shared pointer to an immutable type has value semantics

I am trying to create a builder class which has value semantics which looks like something like this

class Pipeline {
public:
  Pipeline(
     const std::string& name, 
     std::optional<size_t> limitIn,
     std::optional<size_t> limitOut) {...}

  shared_ptr<const Pipeline> limitInput(size limit) const { 
    return make_shared<Pipeline>(name_, size_, limit, limitOut_) ;
  }

  shared_ptr<const Pipeline> limitOutput(size limit) const { 
    return make_shared<Pipeline>(name_, size_, limitInput_, limit) ;
  }
private:
  const string name_;
  const size_t limitInput_;
  const size_t limitOutput_;
};

Since member variables are const, shared_ptr is essentially immutable.

However this pattern breaks down when I need to add inheritance. For ex:


class Pipeline {
 ...

 virtual void doSomething() const = 0;
}

Now in any of the methods (say in limitOutput) when I need to create a new instance of Pipeline I would need to know about the derived class as well since I cannot create an instance of Pipeline anymore. One way I can think of to solve this is to add another virtual method to initialize the object.

class Pipeline {
 ...

 virtual shared_ptr<Pipeline> create(const std::string& name, 
     std::optional<size_t> limitIn,
     std::optional<size_t> limitOut) const = 0;
}

class SpecialPipeline  : public Pipeline {
 ...

 virtual shared_ptr<Pipeline> create(const std::string& name, 
     std::optional<size_t> limitIn,
     std::optional<size_t> limitOut) const override {
    return make_shared<SpecialPipeline>(name, limitIn, limitOut);
 }
};

Now all methods would just defer to this

  shared_ptr<const Pipeline> limitInput(size limit) const { 
    return create(name_, size_, limit, limitOut_);
  }

While this works I personally feel it isn't elegant, involves duplication construction and doesn't feel idiomatic. How would one go about implementing this ? Any feedback would be appreciated.

jack_carver
  • 1,510
  • 2
  • 13
  • 28
  • If you need to clone a polymorphic type there isn't anything better than adding a clone method as c++ doesn't have reflection – Alan Birtles May 26 '20 at 06:38
  • 1
    @AlanBirtles I think the problem is that clone isn't helpful when the members are `const`. You made a copy, but you can't change it. I think OP needs to lose the `const` on members. Mutation can be a private detail, and the signature of `shared_ptr` ensures that non-friends don't mutate the object. – cdhowie May 26 '20 at 06:43

2 Answers2

1

The simplest way to deal with this problem is:

  • Don't expose the constructor; make it protected.
  • Provide factory members on the base type and derived types that return std::unique_ptr<const T> or std::shared_ptr<const T>.
  • Remove the const qualification on your data members.
  • Add a virtual clone method that makes a copy and returns std::unique_ptr<Pipeline>.

From the outside, there is no way to obtain a non-const object since the constructor is not public, so the members do not need to be const.

Since the members are not const, your mutation-factory methods can:

  • Invoke the clone method to make a copy.
  • Mutate the data member on the copy.
  • Extract the pointer from the std::unique_ptr and return a new smart pointer with a const target.
cdhowie
  • 158,093
  • 24
  • 286
  • 300
0

I would use the Curiously Recurring Template Pattern here:

template <class T>
class Pipeline {
public:
  Pipeline(
     const std::string& name, 
     std::optional<size_t> limitIn,
     std::optional<size_t> limitOut) {...}

  shared_ptr<const T> limitInput(size limit) const { 
    return make_shared<T>(name_, limit, limitOut_) ;
  }

  shared_ptr<const T> limitOutput(size limit) const { 
    return make_shared<T>(name_, limitInput_, limit) ;
  }
  ...
};

And in child classes:

class Child: public Pipeline<Child> {
public:

    Child(
     const std::string& name, 
     std::optional<size_t> limitIn,
     std::optional<size_t> limitOut): Pipeline<Child>(name, limitIn, limitOut) {}

    ...
];

That way the child classes have only to have a constructor with the same parameters as their parent and delegate to this one.

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252