这个智能指针包装SDL类型是否安全?

4
我考虑使用如下代码,这样我就无需记得在方法结束时显式调用销毁函数:

(译注:该段代码是关于编程语言中资源的自动管理问题)

#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();
}

我不确定这是否是最佳方法。虽然看起来很简单,但我担心它不能实现我想要的功能。这种方法是否存在任何微妙的错误?


2
win超出作用域时,即在SDL_Quit之后,此代码将调用SDL_DestroyWindow,这可能不是您想要的。 - Holt
@Holt 因为 SDL_CreateWindow 不是子系统,所以应该没问题。此外还有一些解决方法,比如使用 atexit() - user6326610
6
或许对于SDL_init和SDL_Quit,使用RAII结构会有帮助。 - Guillaume Racicot
5个回答

3

引入新的范围,问题应该就解决了:

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();
}

2
如果您想朝着“看起来和感觉像现代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类型,您可以这样做,因为它们都有一个“destroy”函数。(您可能希望将其包装在宏中。)

好处在于,从SDL 2.0.x开始,我们从SDL2 / SDL.h获取的SDL_Window和其他类型都是不完整的类型,即您无法在它们上调用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;
}

2

更多地使用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;
};

并通过因式分解删除器使其DRY:

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;
}

0
编写一个 RAII 类来实现 SDL_InitSDL_Quit
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...)会抛出异常,或者类似的操作。


我不理解你最后的编辑。另外,Args&&...args 不再是转发引用。 - Jarod42

0

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

通过利用上面的观察,我们可以定义一个类模板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可能对应于一个宏,该宏展开为函数调用,而不是函数本身。
您还可以为每种类型创建一个自定义的创建者函数,以便在创建者函数失败时抛出异常。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接