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:
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);
}
}
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.
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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With