C++11:为派生 class 设置成员变量时避免向下转型
C++11: Avoid downcasting when setting member variables for derived class
假设我想构建一个 Car
,其中包含 Motor
、Tire
等组件,它们都是从 "component" 基础 [=48] 派生的=].每个组件都有自己的状态(即电机有 RPM,轮胎有压力等)作为 subclass。一个 Car
class 存储所有组件 classes。
现在我希望 Car
class 有一个 "save car states" 函数,它 returns 一个对象,它循环遍历所有组件的向量并保存所有组件的所有状态组件以便稍后恢复它们。每个组件都应单独负责将其状态存储和恢复到汽车状态对象。
下面的代码是我想出的一个最小示例。但是它在代码中(至少)有两个丑陋的部分,我认为应该避免:
首先,当我想将汽车组件设置回之前保存的状态时,我需要向下转换状态的基础 class。这是一个令人沮丧的有效案例吗?我读到这通常是糟糕设计的标志。因此,我该如何改进呢?我是不是完全误用了多态的概念?
其次,我需要获取 unique_ptr 的原始指针以将状态传回组件并在那里读取它。对我来说也很丑陋。
感谢您的评论!
这是我的代码:
#include <iostream>
#include <vector>
class StateClass{
};
class component{
public:
virtual std::unique_ptr<StateClass> saveStates() = 0;
virtual void loadStates(StateClass* states) = 0;
};
class Motor:public component
{
public:
Motor(){
mstates.rpm = 6000;
mstates.motorstates::oilLevel = 1.0;
}
struct motorstates: public StateClass{
double rpm;
double oilLevel;
void setStates(){
}
};
std::unique_ptr<StateClass> saveStates(){
std::unique_ptr<StateClass> tmp(new motorstates(mstates));
return tmp;
};
void loadStates(StateClass* states){
motorstates* savedState = static_cast<motorstates*>(states); // <=== should this be avoided??
mstates.rpm = savedState->rpm;
mstates.oilLevel = savedState->oilLevel;
};
// private:
void someMethod1();
void someMethod2();
motorstates mstates;
};
class Car{
public:
Car(){
listOfComponents.push_back(&amotor);
}
std::vector<component*> listOfComponents;
// private:
Motor amotor;
std::vector<std::unique_ptr<StateClass>> saveState(){
std::vector<std::unique_ptr<StateClass> > states;
for(auto comp : listOfComponents){
states.push_back(comp->saveStates());
}
return states;
}
void loadState(std::vector<std::unique_ptr<StateClass>>& savedStates){
int cntstates = 0;
for(auto comp: listOfComponents){
comp->loadStates(savedStates.at(cntstates++).get()); // <=== this seems pretty ugly
};
}
};
int main()
{
Car acar;
std::cout << "Car rpm: " << acar.amotor.mstates.rpm << std::endl;
std::cout << "Saving states..." << std::endl;
std::vector<std::unique_ptr<StateClass>> savedState = acar.saveState();
std::cout << "Changing car rpm..." << std::endl;
acar.amotor.mstates.rpm = 5000;
std::cout << "Current car rpm: " << acar.amotor.mstates.rpm << std::endl;
StateClass* tmpState = savedState.at(0).get();
Motor::motorstates* tmpMotorstates = static_cast<Motor::motorstates*>(tmpState);
std::cout << "Saved rpm: " << tmpMotorstates->rpm << std::endl;
std::cout << "Loading state... " << std::endl;
acar.loadState(savedState);
std::cout << "Current car rpm: " << acar.amotor.mstates.rpm << std::endl;
}
补充说明(见评论):
- 我读到了有关访问者 classes 的信息,但它们似乎使事情变得复杂,而不是帮助编写可读代码。
- 我实际上只需要在运行时保存状态,而不是文件
- 汽车只是一个例子。我的真实项目使用了很多具有数百个状态的组件 classes。还有一些州应该恢复,其他州则不会。
设计问题
这些丑陋的东西是由 Component
结构和 State
结构之间的相互依赖性引起的设计问题的症状。
换句话说,你已经定义了一个多态State
,但大多数时候你使用它期望一个特定的状态子class。
另外,汽车的配置其实也是一种状态:如果你对它的配置进行了任何改动,你将无法再恢复任何东西(即使你只是添加了一个无关紧要的备件).
让你的代码更健壮
如果保留这种设计,无论如何都应该使代码更健壮。例如,如果您不小心给出了一个不符合预期的状态指针,会发生什么情况?
motorstates* savedState = static_cast<motorstates*>(states);
这就是 UB!幸运的是你的状态已经是一个多态类型。所以你可以改用动态转换:
motorstates* savedState = dynamic_cast<motorstates*>(states);
if (savedState==nullptr) { // this is true if the state was not of correct class
//ouch ! At least you'd know
}
(顺便说一下,作为一个可以防止出现问题的经验法则:如果你有一个虚拟成员函数,最好给你的 class 一个虚拟析构函数。)
备选设计 1:纪念品
save/restore 能力的一个不错的选择是使用 memento design pattern。这个想法是对象从备忘录中保存或恢复其状态,“看守者”(即负责保存备忘录和调用保管的代码)的内容和结构是未知的。
结果:CarMemento
的内部结构只有 Car 知道。这意味着您将无法恢复部分状态(例如,仅引擎状态)。纪念品不一定是多态对象:保存 state/restore 状态无论如何都会为不同的组件使用特定的纪念品类型。
您的组件似乎是私有成员而不是动态项。如果确认这是最安全的做法。
备选设计 2:复合材料
另一种方法是为您的组件采用 composite design pattern 并将其与纪念品结合起来。
然后我会使用序列化的原则实现状态保存(即使它进入内存对象),并且不仅保存状态而且保存完整的复合结构。然后我会使用工厂反序列化保存的状态。
但是,如果您只想保存对象的状态,则可以将 composite 与 memento 结合使用:
- 要么给 memnto 的内部一个复合结构,假设它总是复制保存的组件。
- 或者用一些唯一的标识来标识车内的部件,将memento构造成一个地图容器,其中returns一个标识符的状态。这种方法的优点是它很灵活,以防您在保存和恢复状态之间添加或删除您关心的组件。
然而,在这两种情况下,您都必须使用动态转换来确保保存状态的类型与要恢复的状态类型相匹配。
假设我想构建一个 Car
,其中包含 Motor
、Tire
等组件,它们都是从 "component" 基础 [=48] 派生的=].每个组件都有自己的状态(即电机有 RPM,轮胎有压力等)作为 subclass。一个 Car
class 存储所有组件 classes。
现在我希望 Car
class 有一个 "save car states" 函数,它 returns 一个对象,它循环遍历所有组件的向量并保存所有组件的所有状态组件以便稍后恢复它们。每个组件都应单独负责将其状态存储和恢复到汽车状态对象。
下面的代码是我想出的一个最小示例。但是它在代码中(至少)有两个丑陋的部分,我认为应该避免:
首先,当我想将汽车组件设置回之前保存的状态时,我需要向下转换状态的基础 class。这是一个令人沮丧的有效案例吗?我读到这通常是糟糕设计的标志。因此,我该如何改进呢?我是不是完全误用了多态的概念?
其次,我需要获取 unique_ptr 的原始指针以将状态传回组件并在那里读取它。对我来说也很丑陋。 感谢您的评论!
这是我的代码:
#include <iostream>
#include <vector>
class StateClass{
};
class component{
public:
virtual std::unique_ptr<StateClass> saveStates() = 0;
virtual void loadStates(StateClass* states) = 0;
};
class Motor:public component
{
public:
Motor(){
mstates.rpm = 6000;
mstates.motorstates::oilLevel = 1.0;
}
struct motorstates: public StateClass{
double rpm;
double oilLevel;
void setStates(){
}
};
std::unique_ptr<StateClass> saveStates(){
std::unique_ptr<StateClass> tmp(new motorstates(mstates));
return tmp;
};
void loadStates(StateClass* states){
motorstates* savedState = static_cast<motorstates*>(states); // <=== should this be avoided??
mstates.rpm = savedState->rpm;
mstates.oilLevel = savedState->oilLevel;
};
// private:
void someMethod1();
void someMethod2();
motorstates mstates;
};
class Car{
public:
Car(){
listOfComponents.push_back(&amotor);
}
std::vector<component*> listOfComponents;
// private:
Motor amotor;
std::vector<std::unique_ptr<StateClass>> saveState(){
std::vector<std::unique_ptr<StateClass> > states;
for(auto comp : listOfComponents){
states.push_back(comp->saveStates());
}
return states;
}
void loadState(std::vector<std::unique_ptr<StateClass>>& savedStates){
int cntstates = 0;
for(auto comp: listOfComponents){
comp->loadStates(savedStates.at(cntstates++).get()); // <=== this seems pretty ugly
};
}
};
int main()
{
Car acar;
std::cout << "Car rpm: " << acar.amotor.mstates.rpm << std::endl;
std::cout << "Saving states..." << std::endl;
std::vector<std::unique_ptr<StateClass>> savedState = acar.saveState();
std::cout << "Changing car rpm..." << std::endl;
acar.amotor.mstates.rpm = 5000;
std::cout << "Current car rpm: " << acar.amotor.mstates.rpm << std::endl;
StateClass* tmpState = savedState.at(0).get();
Motor::motorstates* tmpMotorstates = static_cast<Motor::motorstates*>(tmpState);
std::cout << "Saved rpm: " << tmpMotorstates->rpm << std::endl;
std::cout << "Loading state... " << std::endl;
acar.loadState(savedState);
std::cout << "Current car rpm: " << acar.amotor.mstates.rpm << std::endl;
}
补充说明(见评论):
- 我读到了有关访问者 classes 的信息,但它们似乎使事情变得复杂,而不是帮助编写可读代码。
- 我实际上只需要在运行时保存状态,而不是文件
- 汽车只是一个例子。我的真实项目使用了很多具有数百个状态的组件 classes。还有一些州应该恢复,其他州则不会。
设计问题
这些丑陋的东西是由 Component
结构和 State
结构之间的相互依赖性引起的设计问题的症状。
换句话说,你已经定义了一个多态State
,但大多数时候你使用它期望一个特定的状态子class。
另外,汽车的配置其实也是一种状态:如果你对它的配置进行了任何改动,你将无法再恢复任何东西(即使你只是添加了一个无关紧要的备件).
让你的代码更健壮
如果保留这种设计,无论如何都应该使代码更健壮。例如,如果您不小心给出了一个不符合预期的状态指针,会发生什么情况?
motorstates* savedState = static_cast<motorstates*>(states);
这就是 UB!幸运的是你的状态已经是一个多态类型。所以你可以改用动态转换:
motorstates* savedState = dynamic_cast<motorstates*>(states);
if (savedState==nullptr) { // this is true if the state was not of correct class
//ouch ! At least you'd know
}
(顺便说一下,作为一个可以防止出现问题的经验法则:如果你有一个虚拟成员函数,最好给你的 class 一个虚拟析构函数。)
备选设计 1:纪念品
save/restore 能力的一个不错的选择是使用 memento design pattern。这个想法是对象从备忘录中保存或恢复其状态,“看守者”(即负责保存备忘录和调用保管的代码)的内容和结构是未知的。
结果:CarMemento
的内部结构只有 Car 知道。这意味着您将无法恢复部分状态(例如,仅引擎状态)。纪念品不一定是多态对象:保存 state/restore 状态无论如何都会为不同的组件使用特定的纪念品类型。
您的组件似乎是私有成员而不是动态项。如果确认这是最安全的做法。
备选设计 2:复合材料
另一种方法是为您的组件采用 composite design pattern 并将其与纪念品结合起来。
然后我会使用序列化的原则实现状态保存(即使它进入内存对象),并且不仅保存状态而且保存完整的复合结构。然后我会使用工厂反序列化保存的状态。
但是,如果您只想保存对象的状态,则可以将 composite 与 memento 结合使用:
- 要么给 memnto 的内部一个复合结构,假设它总是复制保存的组件。
- 或者用一些唯一的标识来标识车内的部件,将memento构造成一个地图容器,其中returns一个标识符的状态。这种方法的优点是它很灵活,以防您在保存和恢复状态之间添加或删除您关心的组件。
然而,在这两种情况下,您都必须使用动态转换来确保保存状态的类型与要恢复的状态类型相匹配。