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

用户头像
顿晓
关注
发布于: 2020 年 06 月 14 日
「编程模型」C++资源引用

「编程模型」C++封装资源


引子

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



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



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

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



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

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

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



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

场景

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



代码可以粘贴到https://wandbox.org 在线运行:

#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)」,这样调用者可以放心地使用「指针」来表达一切,而无需当心复杂度增加导致的其他问题。



代码可以粘贴到https://wandbox.org 在线运行:

#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 好处」等关键字。

发布于: 2020 年 06 月 14 日 阅读数: 50
用户头像

顿晓

关注

因观黑白愕然悟,顿晓三百六十路。 2017.10.17 加入

视频号「编程日课」 一个不爱编程的程序员, 一个用软件来解决问题的工程师, 一个有匠心的手艺人。

评论

发布
暂无评论
「编程模型」C++资源引用