「编程模型」C++ 资源引用

蓝月亮
发布于 2020-9-2 18:04
浏览
0收藏

引子
把资源的申请和释放封装到类的构造函数和析构函数中后,调用者的逻辑表达清晰了,但安全性还得程序员手工管理,主要是通过硬编码来控制作用域,尤其要仔细处理异常抛出的情况。

 

处理一般的场景,用上面的方法就足够了,再增加抽象,就会是「杀鸡用牛刀」了。但真碰到需要「庖丁解牛」的复杂场景,就不能手工硬编码了,可以从下面 2 个方面来辅助理解这个转变:

 

其一,解决场景问题仍是最主要的,由规模大导致的复杂度,不能影响解决问题本身,这块额外多出的复杂度可以外包给「领域专家」来解决;

其二,可以类比手工作坊和工厂作业,规模大了必须要引入流水线、自动化这类的技术来使整个流程可控。

 

要操作的资源数量多了之后,最复杂的问题就是资源之间有依赖关系,比如:

依赖关系会调整,这时就容易引发安全问题,增加变更成本;

依赖的范围也多样,有局部的,也有全局的,处理方式也不尽相同。

 

这种情况下,手工编码维护依赖关系,已不是明智之举。

 

场景
C/C++ 是提供「指针」这种方式来解决这类问题的,「指针」在表达方面没什么问题,强大而灵活;但如果要正确处理依赖复杂性导致的安全性问题,难免会出现需要手动处理的特殊情况。

#include <iostream>

using namespace std;

class Resource {
public:
  Resource(size_t size) : size(size), buffer(NULL) {
    init(size);
  }

  ~Resource() {
    release();
  }

  void init(size_t size) {
    if (size > 250) {
      throw "too large!";
    }
    this->size = size;
    this->buffer = new int[size];
    cout << "init size " << this->size << endl;
  }

  void release() {
    cout << "release size " << this->size << endl;
    if (buffer != NULL) {
      delete[] buffer;
      buffer = NULL;
    }
    this->size = 0;
  }

  void DoubleSize() {
    size_t size = this->size;
    release();
    init(size * 2);
  }

  size_t Size() {
    return size;
  }

private:
  size_t size;
  int *buffer;
};

class ResourceA: public Resource {
public:
  ResourceA(size_t size) : Resource(size) {
    cout << "ResourceA ctor\n";
  }
  ~ResourceA() {
    cout << "ResourceA dtor\n";
  }
};

class ResourceB: public Resource {
public:
  ResourceB(size_t size) : Resource(size) {
    cout << "ResourceB ctor\n";
  }
  ~ResourceB() {
    cout << "ResourceB dtor\n";
  }
};

class ResourceAB {
public:
  ResourceAB(ResourceA* a, ResourceB* b): a(a), b(b) {
    cout << "ResourceAB ctor\n";
  }

  ~ResourceAB() {
    cout << "ResourceAB dtor\n";
  }

  void DoubleSize() {
    a->DoubleSize();
    b->DoubleSize();
  }

  void Size() {
    cout << "a " << a->Size() << "; b " << b->Size() << std::endl;
  }
private:
  ResourceA* a;
  ResourceB* b;
};

int main()
{
  ResourceA a(10);
  ResourceB* b = NULL; // 假设 b 的作用域和 a 与 ab 都不同,就需要手动管理资源的申请和释放
  {
    b = new ResourceB(100);
  }
  ResourceAB ab(&a, b);

  try {
    ab.Size();
    ab.DoubleSize();
    ab.Size();
    ab.DoubleSize(); // 这里会触发异常条件
    ab.Size();
  } catch(const char* msg) {
    cerr << msg << endl;
    ab.Size();
  }

  cout << "finally" << endl; // 需要手动处理资源释放
  delete b;  
}

init size 10
ResourceA ctor
init size 100
ResourceB ctor
ResourceAB ctor
a 10; b 100
release size 10
init size 20
release size 100
init size 200
a 20; b 200
release size 20
init size 40
release size 200
too large!
a 40; b 0
finally
ResourceB dtor
release size 0
ResourceAB dtor
ResourceA dtor
release size 40

以上代码只是示意下使用场景,实际项目中的情况会比这个复杂,以至于有可能全部变为手动管理。

 

解决方案
为了彻底解决这种情况,C++ 把这个问题外包给了领域专家「智能指针(std::shared_ptr)」,这样调用者可以放心地使用「指针」来表达一切,而无需当心复杂度增加导致的其他问题。

#include <iostream>
#include <memory>

using namespace std;

class Resource {
public:
  Resource(size_t size) : size(size), buffer(NULL) {
    init(size);
  }

  ~Resource() {
    release();
  }

  void init(size_t size) {
    if (size > 250) {
      throw "too large!";
    }
    this->size = size;
    this->buffer = new int[size];
    cout << "init size " << this->size << endl;
  }

  void release() {
    cout << "release size " << this->size << endl;
    if (buffer != NULL) {
      delete[] buffer;
      buffer = NULL;
    }
    this->size = 0;
  }

  void DoubleSize() {
    size_t size = this->size;
    release();
    init(size * 2);
  }

  size_t Size() {
    return size;
  }

private:
  size_t size;
  int *buffer;
};

class ResourceA: public Resource {
public:
  ResourceA(size_t size) : Resource(size) {
    cout << "ResourceA ctor\n";
  }
  ~ResourceA() {
    cout << "ResourceA dtor\n";
  }
};

class ResourceB: public Resource {
public:
  ResourceB(size_t size) : Resource(size) {
    cout << "ResourceB ctor\n";
  }
  ~ResourceB() {
    cout << "ResourceB dtor\n";
  }
};

class ResourceAB {
public:
  ResourceAB(shared_ptr<ResourceA> a, shared_ptr<ResourceB> b): a(a), b(b) {
    cout << "ResourceAB ctor\n";
  }

  ~ResourceAB() {
    cout << "ResourceAB dtor\n";
  }

  void DoubleSize() {
    a->DoubleSize();
    b->DoubleSize();
  }

  void Size() {
    cout << "a " << a->Size() << "; b " << b->Size() << std::endl;
  }
private:
  shared_ptr<ResourceA> a;
  shared_ptr<ResourceB> b;
};

int main()
{
  auto a = make_shared<ResourceA>(10);
  auto b = make_shared<ResourceB>(100);

  ResourceAB ab(a, b);

  try {
    ab.Size();
    ab.DoubleSize();
    ab.Size();
    ab.DoubleSize(); // 这里会触发异常条件
    ab.Size();
  } catch(const char* msg) {
    cerr << msg << endl;
    ab.Size();
  }

  cout << "finally" << endl;
  // delete b;  // 无需再 手动 处理资源释放
}

init size 10
ResourceA ctor
init size 100
ResourceB ctor
ResourceAB ctor
a 10; b 100
release size 10
init size 20
release size 100
init size 200
a 20; b 200
release size 20
init size 40
release size 200
too large!
a 40; b 0
finally
ResourceAB dtor
ResourceB dtor
release size 0
ResourceA dtor
release size 40

如上,使用「智能指针」一切又回到了美好的样子。

 

C++ 推荐使用「std::make_shared」来构造「std::shared_ptr」,当然是有其充分理由的,如果不想了解具体细节,可以无脑使用:即使用「std::make_shared」来代替 「new」,使用「std::shared_ptr」来代替「*」,代码 Review 时,不能再出现 new 和 *。

 

如果有兴趣了解细节,可以搜索「c++ make_shared silver bullet」或中文「c++ make_shared 好处」等关键字。

分类
收藏
回复
举报
回复
    相关推荐