Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Smart pointer which can change ownership at runtime (C++)

I often encounter a situation when I have complex class (e.g. implementing some numerical algorithm like Partial Differential Equation Solver) with data arrays which it can either own or bind from external context depending on use case. The problem is how to make robust destructor for such class. Simple way is to make boolean flag which indicates wheather the array is owned. E.g.

// simplest example I can think about
class Solver{
   int     nParticles;
   bool own_position;
   bool own_velocity;
   double* position;
   double* velocity;
   // there is more buffers like this, not just position and velocity, but e.g. mass, force, pressure etc. each of which can be either owned or binded externally independently of each other, therefore if there is 6 buffers, there is 2^6 variants of owership (e.g. of construction/destruction) 
   void move(double dt){ for(int i=0; i<n; i++){ position[i]+=velocity[i]*dt; } }

   ~Solver(){
       if(own_position) delete [] position;
       if(own_velocity) delete [] velocity;  
    }
};

Naturally, this motivates to make a template wrapper around the array pointer (should I call it smart pointer ?):

template<typename T>
struct Data{
   bool own;
   T* data;
   ~Data{ if(own)delete [] T; }
}; 


class Solver{
   int          nParticles;
   Data<double> position;
   Data<double> velocity;
   void move(double dt){ for(int i=0; i<n; i++){ position.data[i]+=velocity.data[i]*dt; } }
   // default destructor is just fine (?)
};

Question:

  • This must be common pattern, do I reinvent a wheel here?
  • Is something like this in C++ standard library ? (sorry, I'm rather a physicist than a programmer)
  • are there some catches to think about ?

----------------------------------------

EDIT: To make clear what bind to external contex mean (as Albjenow suggested):

case 1) private/internal work array (no shared ownership)


// constructor to allocate own data
Data::Data(int n){
    data = new double[n];
    own  = true;
}

Solver::Solver(int n_){
    n=n_;
    position(n); // type Data<double>
    velocity(n);
}

void flowFieldFunction(int n, double* position, double* velocity ){
   for(int i=0;i<n;i++){
      velocity[i] = sin( position[i] );
   }
}

int main(){
   Solver solver(100000); // Solver allocates all arrays internally
   // --- run simulation
   // int niters=10;
   for(int i=0;i<niters;i++){
       flowFieldFunction(solver.n,solver.data.position,solver.data.velocity);
       solver.move(dt);
   }
}

case 2) Bind To External data array (e.g. from other class)

Data::bind(double* data_){
    data=data_;
    own=false;
}

// example of "other class" which owns data; we have no control of it
class FlowField{
   int n;
   double* position;
   void getVelocity(double* velocity){
      for(int i=0;i<n;i++){
         velocity[i] = sin( position[i] );
      }
   }
   FlowField(int n_){n=n_;position=new double[n];}
   ~FlowField(){delete [] position;}
}

int main(){
   FlowField field(100000);
   Solver    solver; // default constructor, no allocation
   // allocate some
   solver.n=field.n;
   solver.velocity(solver.n);
   // bind others 
   solver.position.bind( field.position );
   // --- run simulation
   // int niters=10;
   for(int i=0;i<niters;i++){
       field.getVelocity(solver.velocity);
       solver.move(dt);
   }
}
like image 698
Prokop Hapala Avatar asked Dec 03 '19 12:12

Prokop Hapala


2 Answers

Here is a simple way to do what you want without having to write any smart pointer yourself (it would be hard to get the fine details correct) or writing a custom destructor (which would mean more code and bug potential from the other special member functions required by the rule of five):

#include <memory>

template<typename T>
class DataHolder
{
public:
    DataHolder(T* externallyOwned)
      : _ownedData(nullptr)
      , _data(externallyOwned)
    {
    }

    DataHolder(std::size_t allocSize)
      : _ownedData(new T[allocSize])
      , _data(_ownedData.get())
    {
    }

    T* get() // could add a const overload
    {
        return _data;
    }

private:
    // Order of these two is important for the second constructor!
    std::unique_ptr<T[]> _ownedData;
    T* _data;
};

https://godbolt.org/z/T4cgyy

The unique_ptr member holds the self-allocated data, or is empty when externally owned data is used. The raw pointer points to the unique_ptr contents in the former case, or the external content in the latter case. You could modify the constructors (or only make them accessible via static member functions like DataHolder::fromExternal() and DataHolder::allocateSelf() which return DataHolder instances created with the appropriate constructor) to make accidental misuse harder.

(Note that the members are initialized in the order they are declared in the class, not in the order of the member initializer lists, so having the unique_ptr before the raw pointer is important!)

And of course, this class cannot be copied (due to the unique_ptr member) but can be move-constructed or -assigned (with the correct semantics). Works out of the box as it should.

like image 155
Max Langhof Avatar answered Nov 15 '22 10:11

Max Langhof


One solution is to separate data ownership from your solver algorithm. Having the algorithm optionally manage lifetime of its inputs isn't good design because it leads to entanglement of separate concerns. The solver algorithm should always refer to already existing data. And have another extra class that owns data, if necessary, and have lifetime no shorter than that of the algorithm, e.g.:

struct Solver {
    int nParticles;
    double* position;
    double* velocity;
};

struct Data {
    std::vector<double> position, velocity; // Alternatively, std::unique_ptr<double[]>.

    template<class T>
    static T* get(int size, std::vector<T>& own_data, T* external_data) {
        if(external_data)
            return external_data;
        own_data.resize(size);
        return own_data.data();
    }

    double* get_position(int nParticles, double* external_position) { return get(nParticles, position, external_position); }
    double* get_velocity(int nParticles, double* external_velocity) { return get(nParticles, velocity, external_velocity); }
};

struct SolverAndData {
    Data data;
    Solver solver;

    SolverAndData(int nParticles, double* external_position, double* external_velocity)
        : solver{
              nParticles,
              data.get_position(nParticles, external_position),
              data.get_velocity(nParticles, external_velocity)
          }
    {}

    SolverAndData(SolverAndData const&) = delete;
    SolverAndData& operator=(SolverAndData const&) = delete;
};

int main() {
    SolverAndData a(1, nullptr, nullptr);

    double position = 0;
    SolverAndData b(1, &position, nullptr);

    double velocity = 0;
    SolverAndData c(1, nullptr, &velocity);

    SolverAndData d(1, &position, &velocity);
}
like image 28
Maxim Egorushkin Avatar answered Nov 15 '22 10:11

Maxim Egorushkin