在游戏开发领域,我经常看到使用单独的initialize()
和uninitialize()
或shutdown()
方法的类。这不仅包括多个教程,还包括历史悠久的大型实际项目,如一些现代游戏引擎。我最近在Cry Engine 3中看到了一个类,它不仅使用了shutdown()
方法,而且从它调用this.~Foo()
开始,它基于我所知道的关于C ++的一切,实际上并不是真的被认为是一个很好的设计。
虽然我可以看到两步初始化带来的一些好处,并且有很多关于它的讨论,但我无法理解两步销毁背后的原因。为什么不以析构函数的形式使用C ++语言提供的默认工具,但是有一个单独的shutdown()
方法并且析构函数为空?为什么不进一步使用现代C ++,将对象持有的所有资源放入智能指针中,这样我们就不必担心手动释放它们了。
基于不再适用的原则或者是否有一些正当理由将其用于控制对象生命周期的标准方法,两步破坏是否有一些过时的设计?
答案 0 :(得分:3)
如果你不想阅读,那么要点就是你需要例外来从ctors返回错误而异常是坏的。
正如特雷弗和其他人所暗示的那样,这种做法有很多原因。你带来了a specific example here,所以让我们解决这个问题。
本教程涉及一个包含这些定义的类GraphicsClass
(名称肯定不会激发信心):
class GraphicsClass
{
public:
GraphicsClass();
~GraphicsClass();
bool Initialize(int, int, HWND);
void Shutdown();
};
所以它有ctor,dtor和Initialize/Shutdown
。为什么不将后者压缩成前者呢?实现提供了一些线索:
bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
bool result;
// Create the Direct3D object.
m_D3D = new D3DClass;
if(!m_D3D)
{
return false;
}
// Initialize the Direct3D object.
result = m_D3D->Initialize(screenWidth, screenHeight, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR);
if(!result)
{
MessageBox(hwnd, L"Could not initialize Direct3D", L"Error", MB_OK);
return false;
}
return true;
}
确定,检查new D3DClass
是否失败是毫无意义(只有在我们的内存不足和 we've overridden new
to not throw bad_alloc
时才会发生)* 。但是,检查D3DClass::Initialize()
失败可能不是。正如其签名提示,它试图初始化一些与图形硬件相关的资源,这些资源有时会在正常情况下失败 - 可能是请求的分辨率太高,或者资源正在使用中。我们想要优雅地处理它,我们不能在ctor中返回错误,我们只能抛出异常。
这当然提出了一个问题:为什么我们不抛出例外? C++ exceptions are very slow。如此慢的意见are very strong,especially in game development。加上you can't throw in the dtor,试着说,把网络资源终止放在那里很有趣。大多数(如果不是全部)C ++游戏都是在关闭异常的情况下制作的。
这是主要原因;我不能忽视其他的,有时更愚蠢的原因,例如拥有C遗产(没有ctors / dtors),或者具有模块A和B对的架构彼此保持引用。当然要记住游戏开发的#1 priority is to ship games, not create perfectly robust and maintainable architectures,所以你有时会看到这样的愚蠢行为。
我听说C ++委员会非常清楚异常所带来的问题,但最新消息是,它已被置于“太难”的存储桶中,所以你会在游戏中看到更多这样的问题多年来来。
* - 啊哈!因此,检查new D3DClass
是否毫无意义,因为我们可能已禁用异常,因此这是检查内存分配失败的唯一方法,等等。
答案 1 :(得分:2)
如果您的对象已使用placement new分配(即:实例化为未被系统分配为单独块的内存块),则需要显式调用对象的析构函数,如使用delete运算符由于系统试图释放内存块的应用程序指定部分,因此通过智能指针显式或隐式地会失败而非常混乱。
你没有给我们任何足够的信息来推测为什么你提到的特定类可能会明确地调用它的析构函数,猜测这可能是原因是不合理的,并且'Shutdown()'调用是就在那里提供一个围绕显式调用它的析构函数的接口。 (这样引擎的最终用户就不会试图在对象上调用'delete'。可能他们已经将析构函数设为私有,以进一步强制执行他们想要的破坏API。)
答案 2 :(得分:0)
我会使用这种做法(使用DTOR)有几个原因让我有更多的控制和灵活性:
// Passing by value to modify some things without affecting the original instances
somefunc(Foo f, Bar b)
{
// Behind the scene (during construction), the original instances allocated
// and now share some resources with these copies
// Modifying and testing here
.
.
.
// Implicit call to DTOR in 9 .. 8 .. 7
// DTOR was called here implicitly before exiting the scope
// (I may not have actually wanted to free some shared resources)
}
如果我有一个单独的功能,当我想这样做时,我会打电话会更好。 (虽然我不确定这些是否与生成您在问题中引用的代码的特定游戏公司相关。具有实际处理释放资源的单独功能可以让您更灵活地控制这些资源的释放方式和时间。