在以下情况下手动调用析构函数是否是一个糟糕的设计决策?

Is it a bad design decision to manually call the destructor in the following case?

所以我目前正在研究我的 OOP/C++ 技能并偶然发现了以下案例:

我正在制作一个由实体组成的游戏,实体必须进食才能生存。他们可以吃其他实体或食物。为了实现这种行为,我创建了一个 edible 接口,它强制每个实现 class 创建一个 getNutritionalInformation() 方法来计算一个单元在盛宴后的饱和度。

所以整个事情应该像这样在代码方面工作:

std::unique_ptr<Entity> e1(new Entity);
std::unique_ptr<Entity> e2(new Entity);

std::cout << "Pre feasting energy: " << e1->getNutritionalInformation() << std::endl;
e1->eat(*e2);
std::cout << "After feasting energy: " << e1->getNutritionalInformation() << std::endl;

此操作后c1的能量应该比之前更高,这个值目前是在创建实体时随机分配的。但是为了模拟被吃掉的实体的死亡,我想在它被吃掉的时候手动杀死它。我通过以下方式实现了这一目标:

void Entity::eat(Edible& subject) {
   this->energy += subject.getNutritionalInformation();
   delete &subject;
}

但最后这对我来说似乎有点脏,我真的很想让智能指针以某种方式知道,它当前 holds/points 指向的对象不再有效。有没有更清洁的方法来实现这一目标?我很确定我尝试这样做的方式非常 hacky 并且不被认为是正确的 OOP 编程。

提前感谢您的帮助。

是的,请不要手动执行。 这是您实际上 不需要 关心的少数事情之一。您的程序会为您做到这一点! 规则是:一旦变量离开作用域,它就会被丢弃(调用解构函数)。除了动态分配的内存,您还需要释放(删除)variable/s.

此外,如果您手动删除变量,您的程序会崩溃:

{
    Type variable;
    delete variable;
} //Scope end: deconstructor get's called. ERROR: already deleated, undefined behaviour, crash. everything destroyed.

好吧,通过 e2 "eating" e1 它实际上取得了它的所有权。具体来说,它必须拥有它,才能摧毁它。所以实际做法是这样的:

void Entity::eat(std::unique_ptr<Edible> subject) {
    this->energy += subject->getNutritionalInformation();
}

然后...就是这样。主题将在范围结束时自动销毁。为了使用它,您需要显式调用 std::move;这表明您正在将所有权从调用范围转移 到 e2。

std::unique_ptr<Entity> e1(new Entity);
std::unique_ptr<Entity> e2(new Entity);

std::cout << "Pre feasting energy: " << e1->getNutritionalInformation() << std::endl;
e1->eat(std::move(e2));
std::cout << "After feasting energy: " << e1->getNutritionalInformation( << std::endl;

请注意,在 e2 上调用 std::move 后,不能再假设 e2 实际上指向一个实体(因为所有权已转移)。

您希望 e2 的所有者对其变量调用 delete 以防止出现问题。

您可以通过两种方式完成此操作:

e1->eat(std::move(e2));
void Entity::eat(std::unique_ptr<Edible> subject) {
    this->energy += subject->getNutritionalInformation();
}

或者:

e1->eat(*e2);
e2.reset(nullptr);

在任何一种情况下,您的调用者(第一个示例中的函数)都知道 e2 正在被操作破坏,在第一个实例中我给出的是因为您移出了它,在第二个实例中因为它手动调用了 reset (这会破坏旧指针)。

另一种方法是让一些更高级别的管理器控制实体的生命周期,因此 eat 期间的 e2 只会执行:e1->kill(); e1->eatenEnergy(能量); (降低它的营养价值,如果部分被吃掉的实体是可能的)。

然后等到实体管理器轮到它,循环遍历所有实体(或对报告的信号做出反应,例如来自 e1->kill() 实现内部的死亡事件)并释放那些完全死亡的实体 eaten/decayed/etc(杀死那些饥饿的人,...)。

如果您想像当前示例一样保留在基于事件的简单世界中,或者您希望世界中发生一些 "life cycles",而 [memory/instance] 管理,则在很大程度上取决于集中在单个 class.

我认为集中式管理器会使事情变得有点复杂,但是一些可能性应该更容易,比如更好地调试检查 initial/end 回合状态,优先处理一些实体,重用实体的内存( w/o 动态分配)等..

在这两种情况下,您都应该提前考虑一些极端情况(主要是竞争条件)将如何解决,例如:

  • e1 同时想吃 e2,因为 e2 想吃 e1
  • e1和e3同时想吃掉e2
  • e3 在 e2 已经被 e1 吃掉后试图吃掉它(e2 是 "dead" 但已分配)

所以实体的状态应该设计成在这种情况下有效,并以期望的结果结束。

std::unique_ptr 超出范围时,将自动调用对象的析构函数。你不用担心。

手动销毁对象可能会导致问题。在这种情况下,当eat()手动销毁subject对象时,拥有该对象的std::unique_ptr不知道该对象已被销毁,并且在尝试再次销毁该对象时会崩溃第二次。