RAII函数对和模板特化的包装器

7

我为一对初始化和释放资源的 C 函数编写了一个 RAII 包装器,它在大多数情况下都能很好地服务于我。

#include <GL/glfw.h>
#include <string>
#include <functional>
#include <stdexcept>

template <typename UninitFuncType,
          typename SuccessValueType,
          SuccessValueType successValue>
class RAIIWrapper
{
public:
    template <typename InitFuncType, typename... Args>
    RAIIWrapper(InitFuncType initializer,
                UninitFuncType uninitializer,
                const std::string &errorString,
                const Args&... args) : 
        uninit_func(uninitializer)
    {
        if (successValue != initializer(args...))
            throw std::runtime_error(errorString);
        initialized = true;
    }

    bool isInitialized() const
    {
        return initalized;
    }

    ~RAIIWrapper()
    {
        if (initalized)
            uninit_func();
    }

    // non-copyable
    RAIIWrapper(const RAIIWrapper &) = delete;
    RAIIWrapper& operator=(const RAIIWrapper &) = delete;

private:
    bool initalized = false;
    std::function<UninitFuncType> uninit_func;
};

using GLFWSystem = RAIIWrapper<decltype(glfwTerminate), decltype(GL_TRUE), GL_TRUE>;
using GLFWWindow = RAIIWrapper<decltype(glfwCloseWindow), decltype(GL_TRUE), GL_TRUE>;

int main()
{
    GLFWSystem glfw(glfwInit,
                    glfwTerminate,
                    "Failed to initialize GLFW");
}

不过,当一个函数返回void时,比如Enter/LeaveCriticalSection,我不确定该如何在这个类中实现。 我应该为SuccessValueType = void的情况专门化该类吗? 还是使用默认模板参数做些什么?


2
你需要将 SuccessValueTypesuccessValue 作为类模板参数吗?它们不能是构造函数的参数吗?这样,你就可以创建两个单独的构造函数...只是随口一想。 - Andy Prowl
1
那只是另一个参数,不是吗?GL_TRUE应该足够了,它的类型应该被推断出来,所以您不必指定它。 - Andy Prowl
为什么不将这些init和deinit函数也作为模板参数呢?这样,您每种类型只需要一个丑陋的using语句,但实例行会更加清晰。 - leemes
@legends2k:从概念上讲,我认为在构造每个包装器实例时传递返回值(即不作为类的模板参数)更有意义,因为同一包装器类型的每个实例都可能调用不同的初始化函数,并且这可能具有不同的成功代码。 - Andy Prowl
1
欣喜若狂 :) 正确的复制对于RAII始终是相关的。 - Jonathan Wakely
显示剩余8条评论
2个回答

5

我想指出,

  1. 在包装器类中,您不需要关于初始化函数的信息。您只需要知道有关未初始化函数的信息。

  2. 您可以创建函数助手来实例化您的包装器。

我想出了以下解决方案(我喜欢@ipc的异常处理思路)

template <typename UninitF>
struct RAII_wrapper_type
{
    RAII_wrapper_type(UninitF f)
    :_f(f), _empty(false)
    {}
    RAII_wrapper_type(RAII_wrapper_type&& r)
    :_f(r._f), _empty(false)
    {
      r._empty = true;
    }

    RAII_wrapper_type(const RAII_wrapper_type&) = delete;
    void operator=(const RAII_wrapper_type&) = delete;

    ~RAII_wrapper_type()
    {
      if (!_empty) {
        _f();
      }
    }

  private:
    UninitF _f;
    bool _empty; // _empty gets true when _f is `moved out` from the object.
};

template <typename InitF, typename UninitF, typename RType, typename... Args>
RAII_wrapper_type<UninitF> capture(InitF init_f, UninitF uninit_f, RType succ, 
                                   const char* error, Args... args)
{
  if(init_f(args...) != succ) {
    throw std::runtime_error(error);
  }
  return RAII_wrapper_type<UninitF>(uninit_f);
}

template<typename InitF, typename UninitF, typename... Args>
RAII_wrapper_type<UninitF> capture(InitF init_f, UninitF uninit_f, Args... args)
{
  init_f(args...);
  return RAII_wrapper_type<UninitF>(uninit_f);
}

例子:

void t_void_init(int){}
int t_int_init(){ return 1; }
void t_uninit(){}

int main()
{
  auto t1 = capture(t_void_init, t_uninit, 7);
  auto t2 = capture(t_int_init, t_uninit, 0, "some error");
}

编辑

RAII_wrapper_type 应该具有移动语义,我们应该仔细实现其移动构造函数以防止 uninit_f 被多次调用。


这很漂亮和优雅 :) 我看到一个简单的函数解决了这个问题。 - legends2k
你能让这个对象不可复制吗?当我删除了拷贝构造函数和赋值运算符函数时,编译器会抱怨从捕获中返回 RAII_wrapper_type,因为它也是一个副本。 - legends2k
@legends2k,我已经添加了移动问题的解决方案。 - Lol4t0
“_empty” 不是多余的吗?你不能只检查 “_f != nullptr” 吗? - legends2k
我对函数对象很了解,谢谢。但是在你上面的实现中,将nullptr分配给_f可以工作,这意味着编译器将其推断为函数指针。我在GCC和MSVC(msvc没有可变参数模板)中都尝试过,它将RAII_wrapper_type的模板参数推断为C风格的函数指针。 - legends2k
显示剩余3条评论

3
我会分离返回检查和RAII包装的逻辑。
template <typename UninitFuncType>
class RAIIWrapper
{
public:
  template <typename InitFuncType, typename... Args>
  RAIIWrapper(InitFuncType fpInitFunc,
              UninitFuncType fpUninitFunc,
              Args&&... args)
    : fpUninit(std::move(fpUninitFunc))
  {
    static_assert(std::is_void<decltype(fpInitFunc(args...))>::value, "ignored return value");
    fpInitFunc(std::forward<Args>(args)...);
  }

  bool isInitialized() const { return true; } // false is impossible in your implementation

  ~RAIIWrapper() { fpUninit(); } // won't be called if constructor throws

private:
  UninitFuncType fpUninit; // avoid overhead of std::function not needed
};

template <typename InitFuncType, typename UninitFuncType, typename... Args>
RAIIWrapper<UninitFuncType>
raiiWrapper(InitFuncType fpInitFunc,
            UninitFuncType fpUninitFunc,
            Args&&... args)
{
  return RAIIWrapper<typename std::decay<UninitFuncType>::type>
    (std::move(fpInitFunc), std::move(fpUninitFunc), std::forward<Args>(args)...);
}

template <typename InitFuncType, typename SuccessValueType>
struct ReturnChecker {
  InitFuncType func;
  SuccessValueType success;
  const char *errorString;
  ReturnChecker(InitFuncType func,
                SuccessValueType success,
                const char *errorString)
    : func(std::move(func)), success(std::move(success)), errorString(errorString) {}

  template <typename... Args>
  void operator()(Args&&... args)
  {
    if (func(std::forward<Args>(args)...) != success)
      throw std::runtime_error(errorString);
  }
};
template <typename InitFuncType, typename SuccessValueType,
          typename Ret = ReturnChecker<InitFuncType, SuccessValueType> >
Ret checkReturn(InitFuncType func, SuccessValueType success, const char *errorString)
{
  return Ret{func, success, errorString};
}

我还添加了允许类型推断的功能。以下是使用方法:
auto _ = raiiWrapper(checkReturn(glfwInit, GL_TRUE, "Failed to initialize GLFW"),
                     glfwTerminate);

由于具有非空返回值的函数对象会导致静态断言失败,因此以下操作是不可能的:

raiiWrapper(glfwInit, glfwTerminate); // fails compiling

如果您真的想忽略它,可以添加一个ignoreReturn函数对象。同时请注意,返回代码检查可以像您想要的那样复杂(例如成功必须是偶数),因为您可以编写自己的返回代码检查器。


+1 是因为你向我展示了 function<> 不是必需的,并且给出了一个好的答案。我喜欢这个解决方案,但让我们再花点时间,因为我仍然觉得可以在不解耦两个操作的情况下完成。 - legends2k
你确定这个函数不是必需的吗?我尝试编译,clang 抛出了错误:数据成员实例化为函数类型 'void ()'。我认为你打算将其用作函数指针,即 UninitFuncType*。但我并不想这样做,因此我使用了 function - legends2k
1
做一些 std::decay 来修复它。 - Yakk - Adam Nevraumont
那个问题很容易解决,但我更喜欢@Lol4t0提供的解决方案,所以使用它们吧 ;) - ipc
@ipc:我理解了,我猜你可能没有仔细阅读我的注释。ReturnChecker的operator()将始终返回void,这实际上是RAIIWrapper构造函数的输入,并通过static_assert检查其是否为空,那么为什么要检查它的voidness?是的,在调用者直接调用raiiWrapper规避functor的情况下,这是有道理的。 - legends2k
显示剩余4条评论

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