【问题标题】:Template Parameter that can accept function pointer or Functor可以接受函数指针或 Functor 的模板参数
【发布时间】:2017-10-02 16:59:36
【问题描述】:

我正在尝试编写一个符合 RAII 标准的资源包装器,但在如何形成模板参数的语义方面陷入困境。

例如,我可以编写一个函数来删除我的资源:

void int_cleaner(int val) {
    std::cout << "Value of " << val << " has been cleaned up." << std::endl;
}

或者我可以把它写成 Functor:

struct int_deleter {
    void operator()(int val) const {
        std::cout << "Value of " << val << " has been cleaned up." << std::endl;
    }
};

但这就是我卡住的地方:如果我想将它传递给我的资源包装器,我必须更改模板参数的定义方式。

如果我这样写resource

template<typename T, typename Deleter>
class resource {
};

这适用于仿函数,但不适用于函数本身。

int main() {
    resource<int, int_deleter> res; //Compiles fine
    //resource<int, int_cleaner> res2; //Does Not Compile
    return 0;
}

相反,如果我这样写模板参数:

template<typename T>
using deleter_t = void(*)(T);

template<typename T, deleter_t<T> Deleter>
class resource {
};

int main() {
    //resource<int, int_deleter> res; //Does Not Compile
    resource<int, int_cleaner> res2; //Compiles fine
    return 0;
}

现在,我可以编写两个版本的代码,但我不想这样做有两个原因:

  1. 我只是复制resource 的定义,如果我需要对其中一个进行更改,我也需要对另一个进行更改。
  2. 接受函数指针的版本不会接受像void cleaner(T const&amp;) 这样的版本,因为它不会绑定到void(*)(T)。所以我还需要再制作两个或三个版本,以便我可以处理TT&amp;T const&amp;T&amp;&amp;

我怎样才能以最小化代码重复的方式编写资源包装器,特别是考虑到函数版本和函数指针版本之间的删除机制会有所不同?

//example:
template<typename T>
using deleter_t = void(*)(T);

template<typename T, deleter_t<T> Deleter>
class resource {
~resource() {Deleter(val);}
};
template<typename T, typename Deleter>
class resource {
~resource() {Deleter{}(val);}//Note the subtle syntax change
};

【问题讨论】:

  • std::unique_ptr 等。有同样的问题。它们支持按值传递删除器对象,以便您可以传递适当的函数指针。不知道这是否是最好的解决方案。
  • 尝试resource&lt;int, int(&amp;)(int)&gt; 使用int_cleaner 作为删除器。
  • 一种解决方案是创建一个template &lt;auto FUNCTION&gt; struct wrapper,并将其作为模板参数传递给资源:resource&lt;int, wrapper&lt;int_cleaner&gt;&gt;。我认为你必须做出一些妥协,因为int_cleaner 是函数,但int_deleter 是一种类型(因此它们不能同时映射到同一个模板参数)。

标签: c++ templates template-meta-programming


【解决方案1】:

template<typename T, typename Deleter>
class resource {
};

然后写

template<auto k>
using constant_t = std::integral_constant<std::decay_t<decltype(k)>, k>;
template<auto k>
constexpr constant_t<k> constant{};

现在你的 main 看起来像这样:

int main() {
  resource<int, int_deleter> res; //Compiles fine
  resource<int, constant_t<int_cleaner>> res2; //Also compiles fine
  return 0;
}

我们完成了。

Live example.

这是

中,您必须将constant_t&lt;foo&gt; 替换为std::integral_constant&lt;std::decay_t&lt;decltype(foo)&gt;, foo&gt;,因为它缺少auto 模板参数。

integral_constant 中不能使用函数指针,让你调用它们。您必须编写派生类型:

namespace notstd {
  template<class T, T t>
  struct integral_constant:std::integral_constant<T, t> {
    constexpr operator T()const{ return this->get(); }
  }
}

并将std::integral_constant 替换为notstd::integral_constant 以启用该功能。 (隐式转换为函数指针就足以允许在整数常量上使用调用运算符)。

中,你会想要一个新的编译器。


中的另一种方法是使用所有值而不是所有类型。

resource foo(7, int_deleter{});
resource foo2(7, int_cleaner);

并教导资源持有价值给他们的删除者。这会导致int_cleaner 占用存储空间并在资源中具有价值。

resource foo(7, int_deleter{});
resource foo2(7, constant<int_cleaner>);

回到最初的计划,我们通过将int_cleaner 提升到类型系统中来制作无状态指针。

通过使用 EBO,resource 可以以零开销存储无状态删除器。


请注意,您的resource 看起来很多类似于unique_ptr&lt;T, Deleter&gt;,其中Deleter::pointer 是一个薄包装的std::optional&lt;T&gt;(用于可空性)。

template<class T>
struct nullable_opt:std::optional<T> {
  using std::optional<T>::optional;
  nullable_opt( nullable_opt const& ) = default;
  nullable_opt( nullable_opt && ) = default;
  nullable_opt& operator=( nullable_opt const& ) = default;
  nullable_opt& operator=( nullable_opt && ) = default;
  nullable_opt() = default;

  nullable_opt(std::nullptr_t) {}
};

或类似的。

【讨论】:

  • 我确实有一个c++17 编译器,所以这不是问题。我喜欢这个解决方案,尽管我希望有一个版本可以阻止我在本地类型声明中添加(在我看来像样板文件)constant_t 的使用。如果没有,这可能足够干净,我可以可靠地使用它。
  • @Xirema 同一个模板参数不能既是类型又是值。您还可以使用扣除指南和基于值的类型。
  • @Xirema 哦,是的,您可以使用template&lt;typename T, deleter_t&lt;T&gt; Deleter&gt; using resource_fptrdeleter = resource&lt;T, constant&lt;Deleter&gt;&gt;;(选择一个更好的名称)来避免干燥;将编译时函数指针转换为一种类型。
【解决方案2】:

您的模板参数是类型。要从“值”中获取类型,您可以使用decltype。比如说

resource<int, decltype(int_cleaner)> res2;

您仍然需要将实际函数作为参数传递给某个函数(例如构造函数),因为您无法从函数类型创建实例。

我建议您查看执行相同操作的标准类,例如std::unique_ptr。也许您应该使用哪个而不是创建自己的类?或者std::shared_ptr

【讨论】:

  • std::unique_ptrstd::shared_ptr 打算将它们的对象堆分配,这不适合我正在尝试编写的代码。
  • @Xirema 不完全是。他们只打算在指针来自的地方为其对象进行堆分配。您可以在它们上定义自定义删除器,我强烈建议您这样做,而不是编写自己的类。例如,您可以使用 std::unique_ptr 和自定义删除器来包装需要释放资源(即使它不是指针 IIRC)的 C api
  • @Justin 是的,我以前做过。但问题是,一旦超出范围,他们就会行为不端。您最终必须创建一个对象,该对象既维护对象 unique_ptr 的生命周期,这又回到了我最初的问题,即如何编写包装器,只有额外的细节。如果我不在手机上,我会给你举一个激励人心的例子。
  • @Xirema 您不应该将智能指针视为简单的自动删除指针,而是更多地从所有权的角度来看。通过它们,您可以拥有 资源 的单一或共享所有权,这要归功于 RAII,它会在需要时自动释放。
  • 我知道如何思考unique_ptrshared_ptr。您需要将其视为既定事实,在考虑了这些选项后,我仍然确定它们不适合我的特定问题。
猜你喜欢
  • 2016-03-15
  • 2021-10-18
  • 1970-01-01
  • 2013-06-06
  • 2013-04-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-02-21
相关资源
最近更新 更多