【问题标题】:Smart pointer which can change ownership at runtime (C++)可以在运行时更改所有权的智能指针 (C++)
【发布时间】:2020-03-28 03:28:17
【问题描述】:

当我有复杂的类(例如,实现一些数值算法,如偏微分方程求解器)时,我经常遇到这样的情况,它可以根据用例拥有或从外部上下文绑定。问题是如何为此类创建健壮的析构函数。简单的方法是制作布尔标志,指示数组是否拥有。例如

// 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;  
    }
};

自然地,这促使我们围绕数组指针创建一个模板包装器(我应该称之为智能指针吗?):

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 (?)
};

问题:

  • 这一定是常见的模式,我在这里重新发明轮子吗?
  • C++ 标准库中有这样的东西吗? (对不起,我是物理学家而不是程序员)
  • 是否有一些需要考虑的问题?

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

编辑:明确bind to external contex 的含义(正如Albjenow 建议的那样):

案例 1)私有/内部工作数组(无共享所有权)


// 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);
   }
}

案例2)绑定到外部数据数组(例如来自其他类)

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);
   }
}

【问题讨论】:

  • 一个std::shared_ptr?
  • 从技术上讲,您可以将std::unique_ptrstd::shared_ptr 与自定义删除器一起使用,该删除器存储是删除(您拥有它)还是什么都不做(外部拥有)。编写自己的课程也很有效,但要在各个方面做到正确需要一些经验......
  • 还有提升intrusive_ptr
  • 你能举一个“从外部上下文绑定”的例子吗?在不了解您的用例的情况下,我们很难提出解决方案。
  • UniversE > “其他实例”不必知道。如果数据数组为own=True 所拥有,则根本不应从外部访问该数组(或者如果由用户负责处理该问题)。这不是这里的任务。这里的任务是创建可以分配自己的工作数组或绑定到已经存在的工作数组的类。

标签: c++ destructor smart-pointers ownership


【解决方案1】:

这是一种简单的方法来做你想做的事,而无需自己编写任何智能指针(很难得到正确的细节)或编写自定义析构函数(这意味着更多的代码和潜在的错误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

unique_ptr 成员保存自己分配的数据,或者使用外部拥有的数据时为空。在前一种情况下,原始指针指向unique_ptr 内容,在后一种情况下指向外部内容。您可以修改构造函数(或仅通过 DataHolder::fromExternal()DataHolder::allocateSelf() 等静态成员函数访问它们,这些函数返回使用适当构造函数创建的 DataHolder 实例)以使意外误用更加困难。

(请注意,成员是按照它们在类中声明的顺序进行初始化的,而不是按照成员初始化列表的顺序进行初始化,因此在原始指针之前有 unique_ptr 很重要!)

当然,这个类不能被复制(由于unique_ptr 成员),但可以被移动构造或赋值(使用正确的语义)。开箱即用。

【讨论】:

  • 谢谢,我明白你的意思。但我不确定我是否喜欢这个。因为它增加了一些复杂性(它就像一个包装器的包装器)。如果标准库中有即时罐头解决方案,我可能会使用它。但是当我必须编写自己的包装器时,我更喜欢从头开始,因为我不喜欢使用一些我不知道它具体做什么、如何实现的东西(unique_ptr)。对于特定的用例(做一些不必要的工作,并使用不必要的内存),它可能效率低下,我无法控制。对不起,也许我太老派 C 人了。
  • 没有固定的解决方案,这可能是您可以添加的最低程度的复杂性(没有像其他答案中建议的那样回避整个问题)。如果您不了解此包装器的作用,请随时提问。这里没有什么神奇之处,考虑到现代 C++ 的基本知识,一切都应该很清楚。此解决方案几乎不使用额外空间,也不执行额外工作 - 您最终会像以前一样使用原始 T*
  • 我了解您编写的包装器的作用。我不知道unique_ptr 是如何实现的(猜想它取决于平台)。对我来说,所有智能指针都是原始指针的包装器。所以写智能指针的包装器就是写包装器的包装器。从使意外误用更难之类的句子中,我认为您在不同的上下文中思考。我不是在尝试用户做某事。我正在尝试使用最少的样板来制作最灵活的类。添加“五法则” - 好吧,也许我会在某个时候碰壁,但我从来不必遵循。
  • 我强烈建议您习惯使用std::unique_ptr,这样您就可以减轻对空间或性能开销的恐惧(两者都没有)。它所做的只是 1. 表达(在语义层面上)您唯一拥有一个资源,以及 2. 在销毁时注意释放该资源。而已。只是它是一个包装器并不意味着它更慢或更大或任何类似的东西。如果您不相信我,请查看生成的程序集。
  • 很高兴知道。在这种情况下,你是对的。我正在考虑在一段时间内习惯使用智能指针。但是关于它们实现的不确定性,以及我真正需要它们(或发现它们方便)的用例的缺乏总是让我望而却步。就美学和人体工程学而言,我更喜欢*,而不是std::unique_ptr&lt;&gt;,因为它更短(代码中的视觉噪音更少)。
【解决方案2】:

一种解决方案是将数据所有权与您的求解器算法分开。让算法有选择地管理其输入的生命周期并不是一个好的设计,因为它会导致不同关注点的纠缠。求解器算法应始终参考已经存在的数据。如有必要,还有另一个拥有数据的额外类,并且生命周期不短于算法的生命周期,例如:

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);
}

【讨论】:

  • 好的,但这需要不必要的样板和污染命名空间
  • @ProkopHapala 为您添加了一个示例。我不认为不必要的样板和污染命名空间适用于此。
  • 当所有工作缓冲区(不仅仅是positionvelocity)可以是自己的或外部绑定的情况下,自动处理所有变化的构造函数和析构函数怎么样?如果有 6 个这样的缓冲区,我需要 2^6 个不同的包装器吗?不是说我写在标题里,我更喜欢切换所有权运行时。
  • @ProkopHapala SolverAndData 拥有示例中的输入。当不需要输入所有权时,只需单独使用Solver
  • @ProkopHapala 为您添加了另一个示例。
猜你喜欢
  • 2014-07-25
  • 2011-05-27
  • 2016-03-18
  • 2020-03-16
  • 1970-01-01
  • 2017-04-29
  • 1970-01-01
  • 2014-09-25
  • 1970-01-01
相关资源
最近更新 更多