问题提出:
我们做事一般都有一套完整的流程比如:脱衣服——洗澡——穿衣服,如果用户操作直接不脱衣服从洗澡开始,那么程序可能就会出问题,例如定义一个一般的类模拟推理过程:
//提出问题
class Infer{
public:
bool load_model(const string& file){
context_ = file;
return true;
}
void forward(){
printf("使用%s进行推理\n",context_.c_str());
}
void destroy(){
context_.clear();
}
private:
string context_;
};
如果创建完对象上来就直接推理(跳过了context_初始化步骤):
int main()
{
Infer infer;
//1. 如果直接对对象进行推理
infer.forward();
return 0;
}
此时返回:
此时还能正常运行,因为这里context_未初始化时为空string对象,如果是个未初始化的空指针,那么程序会异常。
因此按照一般思维步骤,需要在当前调用之前进行判断:
void forward(){
//异常逻辑
if(context_.empty()){
//说明模型没加载上
//此时需要写异常情况处理的定义(可能会比较复杂繁琐)
printf("模型没有加载\n");
return;
}
//正常逻辑
printf("使用%s进行推理\n",context_.c_str());
}
为什么会出现异常逻辑呢,因为我们并没有显式说明不能这么操作(这里指未初始化直接推理),那么有两种解决方法,一种是频繁告诉用户不能怎么怎么操作(这种方法对用户不友好,异常逻辑会耗费大量的时间,且如果有部分异常处理没有写好会造成封装的不安全性,导致崩溃,也会造成使用的复杂度变高),二是仅仅开放正常逻辑应该有的操作接口(RAII(资源获取即初始化) + 接口模式(实现类和接口类分离的模式))。
RAII(资源获取即初始化):
例如例子中:
int main()
{
//资源获取
Infer infer;
//资源初始化
infer.load_model("a");
return 0;
}
两个步骤分别对应了资源获取和资源初始化,RAII要做的就是将两步合为一步,通过共享指针来完成:
//RAII
//获取infer实例,即表示加载模型
//加载模型失败则表示资源获取失败
//加载模型成功则表示资源获取成功
shared_ptr<Infer> create_infer(const string& file){
shared_ptr<Infer> instance(new Infer());
//如果load_model失败则instance为空指针
if(!instance->load_model(file)){
instance.reset();
}
return instance;
}
此时infer实例通过create_infer来完成,得到的instance不为空则说明获取资源成功且同时完成了模型加载工作,如果instance为空,则说明获取资源失败,简化了后续forward等的判断流程,可以删掉是否只加载了一次模型以及是否加载了模型等的异常逻辑判断:
int main()
{
// //资源获取
// Infer infer;
// //资源初始化
// infer.load_model("a");
auto infer = create_infer("a");
//只需要简单的是否为空指针的判断
if(infer == nullptr){
printf("failed");
return -1;
}
infer->forward();
return 0;
}
进一步和接口的配合可以实现模型只在create_infer中完成,且只完成一次,我们通过一个接口类InferInterface来实现接口类和定义类的分离,我们只希望暴露给用户forward函数:
//接口类是一个纯虚类
//只暴露调用者需要的函数,其他一律不暴露
//例如load_model通过RAII做了定义,属于不需要的范畴
//内部如果有启动线程等等,start,stop,也不需要暴露
class InferInterface{
public:
virtual void forward() = 0;
};
将Infer类作为InferInterface的派生类并重写forward函数:
class Infer : public InferInterface{
public:
bool load_model(const string& file){
context_ = file;
return true;
}
virtual void forward() override{
//正常逻辑
printf("使用%s进行推理\n",context_.c_str());
}
void destroy(){
context_.clear();
}
private:
string context_;
};
在RAII中将返回类型改为接口类对象:
shared_ptr<InferInterface> create_infer(const string& file){
shared_ptr<Infer> instance(new Infer());
//如果load_model失败则instance为空指针
if(!instance->load_model(file)){
instance.reset();
}
return instance;
}
此时暴露给用户的只有forward函数。