SDL 类型的智能指针包装器是否安全



我想过使用类似以下内容的东西,这样我就不必记得在方法末尾显式调用破坏者函数:

#include <iostream>
#include <SDL2/SDL.h>
#include <memory>
int main()
{
    SDL_Init(SDL_INIT_VIDEO);
    std::unique_ptr<SDL_Window, decltype((SDL_DestroyWindow))>
        win { SDL_CreateWindow("asdf", 100, 100, 640, 480, SDL_WINDOW_SHOWN),
                  SDL_DestroyWindow };
    if (!win.get())
    {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow Error: %s",
                SDL_GetError());
        return 1;
    }
    SDL_Quit();
}

我不确定这是否是最好的方法。我担心这不能做我想要它做的事情,即使它看起来很简单。这种方法有什么微妙的错误吗?

引入一个新的作用域,你应该没问题:

int main()
{
  SDL_Init(SDL_INIT_VIDEO);
  {
    std::unique_ptr<SDL_Window, decltype((SDL_DestroyWindow))>
      win { SDL_CreateWindow("asdf", 100, 100, 640, 480, SDL_WINDOW_SHOWN),
        SDL_DestroyWindow };
    if (!win.get())
    {
      SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow Error: %s",
          SDL_GetError());
      return 1;
    }
  } // win destroyed before SQL_Quit
  SDL_Quit();
}

更多地使用 RAII:

struct SDL_RAII
{
    SDL_RAII() { SDL_Init(SDL_INIT_VIDEO); }
    ~SDL_RAII() noexcept {
        try {
            SDL_Quit();
        } catch (...) {
            // Handle error
        }
    }
    SDL_RAII(const SDL_RAII&) = delete;
    SDL_RAII(SDL_RAII&&) = delete;
    SDL_RAII& operator=(const SDL_RAII&) = delete;
    SDL_RAII& operator=(SDL_RAII&&) = delete;
};

并通过分解删除器来干燥:

template <typename Object, void (*DeleterFun)(Object*)>
struct Deleter
{
    void operator() (Object* obj) const noexcept
    {
        try {
            DeleterFun(obj);
        } catch (...) {
            // Handle error
        }
    }
};
template <typename Object, void (*DeleterFun)(Object*)>
using UniquePtr = std::unique_ptr<Object, Deleter<Object, DeleterFun>>;

然后,SDL 的一些类型:

using Unique_SDL_Window = UniquePtr<SDL_Window, SDL_DestroyWindow>;
using Unique_SDL_Surface = UniquePtr<SDL_Surface, SDL_FreeSurface>;
// ...

最后:

int main()
{
    SDL_RAII SDL_raii;
    Unique_SDL_Window win{ SDL_CreateWindow("asdf", 100, 100, 640, 480, SDL_WINDOW_SHOWN)};
    if (!win.get())
    {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                     "SDL_CreateWindow Error: %s",
                     SDL_GetError());
        return 1;
    }
    return 0;
}
如果你想让它

"看起来像现代C++",你可以结合以下两种技术:

  • 示波器防护(用于SDL_Quit(,
  • 专用默认删除器(用于SDL_Window(。

示波器防护罩

作用域防护是一个虚拟对象,它从其析构函数调用提供的函数对象。在堆栈上分配它将使其在离开定义范围时调用析构函数;在 main() 函数中执行此操作意味着它将在程序退出时运行。有关更多详细信息,Andrei Alexandrescu对此进行了很好的演讲(关于范围防护的部分从1:05:14开始(。

实现(主要来自演示文稿(:

template<class Fn>
class ScopeGuard {
 public:
  explicit ScopeGuard(Fn fn) noexcept
      : fn_(std::move(fn)),
        active_(true) {}
  ScopeGuard() = delete;
  ScopeGuard(const ScopeGuard &) = delete;
  ScopeGuard(ScopeGuard &&that) noexcept
      : fn_(std::move(that.fn_)),
        active_(that.active_) {
    that.dismiss();
  }
  ScopeGuard &operator=(const ScopeGuard &) = delete;
  ~ScopeGuard() {
    if (active_) {
      try {
        fn_();
      } catch (...) {
        // The destructor must not throw.
      }
    }
  }
  void dismiss() noexcept {
    active_ = false;
  }
 private:
  Fn fn_;
  bool active_;
};

直接实例化类不是很方便,但是在函数上我们得到了类型推断:

// Provided purely for type inference.
template<class Fn>
ScopeGuard<Fn> scopeGuard(Fn fn) {
  return ScopeGuard<Fn>(std::move(fn));
}

若要创建作用域保护,只需调用scopeGuard(lambda)其中lambda是要在离开作用域时运行的函数。将推断实际类型;反正我们对它不感兴趣。

// Will call SDL_Quit() once 'guard' goes out of scope.
auto guard = scopeGuard([] { SDL_Quit(); });

专用默认删除程序

您可以通过定义以下函数对象(在本例中为带有 operator() 的结构(来专门化std::default_deleter

template<>
struct std::default_delete<SDL_Window> {
  void operator()(SDL_Window *p) { SDL_DestroyWindow(p); }
};

您可以对 SDL 中的大多数类型执行此操作,因为它们只有一个"销毁"功能。(您可能希望将其包装在宏中。

这样做的好处是,从 SDL 2.0.x 开始,我们从 SDL_Window 和其他类型中获取它们SDL2/SDL.h它们是不完整的类型,即您无法对它们调用sizeof(SDL_Window)。这意味着编译器将无法开箱即用地delete它们(因为它需要sizeof(,因此您将无法实例化(普通(std::unique_ptr<SDL_Window>

但是,使用专门的删除器,std::unique_ptr<SDL_Window>起作用,并将在析构函数中调用SDL_DestroyWindow

结果

使用上面的定义,结果是 RAII 的一个无样板示例:

int main()
{
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        return 1;
    }
    auto quit_scope_guard = scopeGuard([] { SDL_Quit(); });
    std::unique_ptr<SDL_Window> win(SDL_CreateWindow("asdf", 100, 100,
                                    640, 480, SDL_WINDOW_SHOWN));
    if (!win) {
        // ~quit_scope_guard will call SDL_Quit().
        return 1;
    }
    // ~win will call SDL_DestroyWindow(win.get()).
    // ~quit_scope_guard will call SDL_Quit().
    return 0;
}

SDL_InitSDL_Quit写为 RAII 类:

struct SDL_exception {
  int value;
};
struct SDL {
  SDL(uint32_t arg) {
    if(int err = SDL_INIT(arg)) {
      throw SDL_exeption{err};
    }
  }
  ~SDL() {
    SDL_Quit();
  }
};

接下来,创建一个智能指针制作器:

template<class Create, class Destroy>
struct SDL_factory_t;
template<class Type, class...Args, class Destroy>
struct SDL_factory_t<Type*(*)(Args...), Destroy> {
  using Create=Type*(*)(Args...);
  Create create;
  Destroy destroy;
  using type=Type;
  using ptr = std::unique_ptr<Type, Destroy>;
  ptr operator()(Args&&...args)const {
    return {create(std::forward<Args>(args)...), destroy};
  }
};
template<class Create, class Destroy>
constexpr SDL_factory_t<Create,Destroy>
SDL_factory(Create create, Destroy destroy) {
  return {create, destroy};
};

这会将定义破坏的仪器放在一个位置,而不是在您创建事物的每个位置。

对于要包装的每种类型,只需执行以下操作:

constexpr auto SDL_Window_Factory = SDL_factory(SDL_CreateWindow, SDL_DestroyWindow);

现在你的代码变成:

int main()
{
  SDL init(SDL_INIT_VIDEO);
  auto win = SDL_Window_Factory("asdf", 100, 100, 640, 480, SDL_WINDOW_SHOWN);
  if (!win) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow Error: %s",
            SDL_GetError());
    return 1;
  }
}

这在main体内的噪音要小得多,不是吗?

作为奖励,窗口(和其他任何内容(在 SDL 退出命令之前被销毁。

我们可以在工厂中添加更多基于异常的错误处理,甚至可以以某种方式使其成为一个选项,.notthrowing( args... )不会抛出,而( args... )会抛出,或者诸如此类。

如果您看一下 SDL 库中类型的实例是如何创建和销毁的,您会发现它们通常是通过调用返回指向新创建的实例的指针的函数来创建的。通过调用一个函数来销毁它们,该函数将指针指向要销毁的实例。例如:

  • 一个SDL_Window实例是通过调用 SDL_CreateWindow() 创建的,它返回一个SDL_Window *,并用 SDL_DestroyWindow() 销毁,这需要一个SDL_Window *

  • 一个SDL_Renderer实例是通过调用 SDL_CreateRenderer() 创建的,它返回一个SDL_Renderer *,并用 SDL_DestroyRenderer() 销毁,这需要一个SDL_Renderer *

通过利用上面的观察结果,我们可以定义一个类模板 scoped_resource ,它实现了 SDL 类型的 RAII 习惯用法。首先,我们定义以下便利函数模板,这些模板充当 SDL_CreateWindow()SDL_DestroyWindow() 等函数的类型特征:

template<typename, typename...> struct creator_func;
template<typename TResource, typename... TArgs>
struct creator_func<TResource*(*)(TArgs...)> {
   using resource_type = TResource;
   static constexpr auto arity = sizeof...(TArgs);
};
template<typename> struct deleter_func;
template<typename TResource>
struct deleter_func<void(*)(TResource*)> {
   using resource_type = TResource;
};

然后,scoped_resource类模板:

#include <memory> // std::unique_ptr
template<auto FCreator, auto FDeleter>
class scoped_resource {
public:
   using resource_type = typename creator_func<decltype(FCreator)>::resource_type;
   template<typename... TArgs>
   scoped_resource(TArgs... args): ptr_(FCreator(args...), FDeleter) {}
   operator resource_type*() const noexcept { return ptr_.get(); }
   resource_type* get() const noexcept { return ptr_.get(); }
private:
   using deleter_pointee = typename deleter_func<decltype(FDeleter)>::resource_type;
   static_assert(std::is_same_v<resource_type, deleter_pointee>);
   std::unique_ptr<resource_type, decltype(FDeleter)> ptr_;
};

现在可以使用类型别名引入以下类型:

using Window   = scoped_resource<SDL_CreateWindow,   SDL_DestroyWindow>;
using Renderer = scoped_resource<SDL_CreateRenderer, SDL_DestroyRenderer>;

最后,您可以通过构造Window对象来创建SDL_Window实例。你传递给Window的构造函数的参数与传递给SDL_CreateWindow()的参数相同:

Window win("MyWindow", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, 0);

当对象win被销毁时,将在包含的SDL_Window *上调用SDL_DestroyWindow()


但是请注意,在某些情况下 - 例如,在创建SDL_Surface时,您可能需要将原始创建者函数包装在您自己的函数中,并将此函数作为创建者传递scoped_resource。这是因为SDL_LoadBMP可能对应于扩展到函数调用而不是函数的宏。

您可能还希望为每个类型创建自定义创建者函数,以便在创建者函数失败时能够引发异常。

最新更新