SDL 中错误处理和清理资源的良好做法?

Good practice for error handling and cleaning up resources in SDL?

在使用 SDL 时,我一直在处理错误(例如在 SDL 初始化期间),每当函数中遇到错误时返回 false,否则返回 true。然后通过在 main() 函数的最后调用 close() 函数来执行清理,例如:

int main(){
    if(!init()){
        // do some stuff
    }

    ...

    close();
    return 0;
}

close()函数:

void close(){
    SDL_DestroyRenderer(g_Renderer);
    SDL_DestroyWindow(g_Window); 

    IMG_Quit();
    SDL_Quit();
}

但是,我意识到这种清理方法意味着我不能真正抛出异常,因为 close() 函数不会被调用。 在做了一些阅读之后,我决定尽量不抛出异常,除非需要,出于与性能相关的原因(我听说这很重要,尤其是在游戏开发中)。

此外,这意味着随着程序的增长,我需要在 close() 上添加更多清理功能,这似乎不切实际且容易忘记。

所以我的问题是:

  1. 一般来说,我们在使用SDL时应该在什么时候抛出异常?除非需要,否则通常避免使用它们是否被认为是好的做法?
  2. 为了允许使用异常,应该如何清理 SDL 资源?我看过一些方法,但我不确定什么时候会使用每一种方法,或者结合每种方法:
    • 创建一个 C++ 包装器 class 来处理构造函数中的初始化和析构函数中的清理工作
    • 通过正常初始化 SDL 对象使用 unique_ptr(或其他智能指针)并提供删除器 class 进行清理。
    • 使用atexit()来处理SDL_Quit()IMG_Quit()?

我正在考虑实现一个包装器 class,但后来意识到我可能需要在初始化过程中出现错误时抛出异常,例如来自 SDL_CreateWindow(),这是我试图避免的。有什么帮助或建议吗?

  1. Use atexit() to handle SDL_Quit() and IMG_Quit()?

我不会使用 atexit。其他替代方案可以让您更好地控制何时进行清理。

  1. Use unique_ptr (or other smart pointers) by initialising SDL object normally and provide a deleter class for cleaning-up.

它可以工作,但我不喜欢这种方法。它可以用于清理诸如 SDL_Window * 之类的指针,但是如果您需要清理的不是指针怎么办? (例如,如果您想自动调用 SDL_Quit()。)

使用 unique_ptr 清理指针和其他清理其他东西的方法会无缘无故地在代码中引入不一致,所以我会完全避免它(内存管理除外)。

  1. Create a C++ wrapper class to handle both initialisation in the constructor and cleaning up in its destructor

这是个好主意。

如果你要去做,别忘了关注the rule of three。要么删除复制构造函数和赋值运算符:

MyWindow(const MyWindow &) = delete;
MyWindow &operator=(const MyWindow &) = delete;

或者让 class 可移动且不可复制,以使其更易于使用。

  1. 瞄准镜守卫。

还有另一种选择:作用域守卫。这个想法是将清理代码放入局部变量的析构函数中。

这些局部变量通常是使用宏创建的。这是我使用的:

#include <exception>
#include <utility>

namespace Macro
{
    template <typename T> class FinallyObject
    {
        T func;

      public:
        FinallyObject(T &&func) : func(std::move(func)) {}
        FinallyObject(const FinallyObject &) = delete;
        FinallyObject &operator=(const FinallyObject &) = delete;
        ~FinallyObject()
        {
            func();
        }
    };
}

#define FINALLY_impl_cat(a, b) FINALLY_impl_cat_(a, b)
#define FINALLY_impl_cat_(a, b) a##b

#define FINALLY(...) \
    ::Macro::FinallyObject FINALLY_impl_cat(_finally_object_,__LINE__) ([&]{ __VA_ARGS__ });

下面是您如何使用它们:

int main(int, char **)
{
    if (SDL_Init(...))
        /*throw or exit with an error*/;
    FINALLY( SDL_Quit(); )

    SDL_Window *window = SDL_CreateWindow(...);
    if (!window)
        /*throw or exit with an error*/;
    FINALLY( SDL_DestroyWindow(window); )

    // Your code here.
}
  1. 如评论中所建议,考虑在程序退出之前不清理使用的资源。

对于程序退出之前使用的资源,应该不需要清理。 OS 会自动清理它们。 (SDL_QuitSDL_DestroyWindow 等)


In general, when should we raise exceptions when using SDL? Is it considered good practice to generally avoid them unless needed?

如果你正确地清理(使用析构函数 and/or 作用域守卫,而不是手动),异常将使这方面的事情变得更容易,而不是更难。

在您实际投掷之前,它们不应该对性能产生太大影响,因此如果您只在极少数情况下投掷,应该没问题。但它们确实会增加二进制文件的大小。