【问题标题】:Using automatic deduction with unique_ptr and custom deleter通过 unique_ptr 和自定义删除器使用自动扣除
【发布时间】:2023-03-11 20:37:01
【问题描述】:

我目前正在使用一个定义了许多数据类型的 C 库,所有这些数据类型都需要由用户管理它们的生命周期。有许多以这种方式定义的函数:

int* create() {
    return new int();
}

void destroy(int* i) {
    delete i;
}

其中大部分不需要在创建后访问。他们只需要存在。正因为如此,我正在尝试使用 unique_ptr 在我需要它们生存的范围内声明的来管理它们。

这样的声明是这样的:

// Note that I'm avoiding writing the type's name manually.
auto a = std::unique_ptr<std::decay_t<decltype(*create())>, decltype(&destroy)>{create(), &destroy};

但是这太冗长了,所以我把它封装在一个实用函数模板中:

template<typename T>
auto make_unique_ptr(T* p, void (*f)(T*)) {
    return std::unique_ptr<T, decltype(f)>(p, f);
}

这样用的:

auto b = make_unique_ptr(create(), &destroy);

这看起来不错,但引入了一个非标准函数,除了作为某些声明的语法糖之外没有任何实际用途。我的同事甚至可能不知道它的存在,并最终创建了具有不同名称的其他版本。

介绍了class template argument deduction。我认为这是解决我的问题的完美解决方案:一种推断所有这些类型的标准方法,而无需求助于用户定义的包装器。所以我尝试了这个:

auto c = std::unique_ptr{create(), &destroy};

按照 C++ 编译器和模板的规则,这会失败并显示几行长的错误消息。以下是相关部分:

(...): error: class template argument deduction failed:
 auto c = std::unique_ptr{create(), &destroy};
                                            ^
(...): note: candidate: 'template<class _Tp, class _Dp> 
unique_ptr(std::unique_ptr<_Tp, _Dp>::pointer, typename std::remove_reference<_Dp>::type&&)-> std::unique_ptr<_Tp, _Dp>'
       unique_ptr(pointer __p,
       ^~~~~~~~~~
(...): note:   template argument deduction/substitution failed:
(...): note:   couldn't deduce template parameter '_Tp'
     auto c = std::unique_ptr{create(), &destroy};
                                                ^

理论上,我可以添加一个演绎指南来处理这个问题:

namespace std {

template<typename T>
unique_ptr(T* p, void (*f)(T*)) -> unique_ptr<T, decltype(f)>;

}

至少在我的 gcc 版本上它确实有效,但标准不太喜欢它:

[命名空间.std]

1 除非另有说明,否则如果 C++ 程序向命名空间 std 或命名空间 std 内的命名空间添加声明或定义,则其行为未定义。

4 C++ 程序的行为是未定义的,如果它声明
(...)
4.4 - 任何标准库类模板的推导指南。

还有一些与区分指针和数组有关的问题,但让我们忽略它。

最后,问题:在使用自定义删除器时,是否有任何其他方法可以“帮助”std::unique_ptr(或者可能是std::make_unique)来推断正确的类型?以防万一这是一个 XY 问题,对于这些类型的生命周期管理,是否有任何我没有想到的解决方案(可能是 std::shared_ptr)?如果这两个答案都是否定的,那么 是否有任何改进我应该期待,以解决这个问题?

Feel free to test the above examples at Coliru.

【问题讨论】:

  • FWIW,除非您需要在运行时更改删除器,否则不要使用这样的函数指针删除器。没有它,使用显式类型sizeof(unique_ptr&lt;T&gt;) == sizeof(T*)。有了它,sizeof(unique_ptr&lt;T&gt;) == sizeof(T*) + sizeof(deleter),在这种情况下是两倍大小
  • 为什么不使用make_library_type_nnn() 来返回正确的unique_ptr 并调用正确的create 函数?
  • @NathanOliver 恕我直言,它的扩展性不是很好。这些类型有很多(超过 100 种),而且大多数都有多种创建方式。
  • @Justin 感谢您的信息。这可能并不重要(实际结构比单个int 复杂一点)。我可能必须测量差异以确定,但这是我现在试图避免的大量工作。
  • template&lt;auto f&gt;using deleter_t = std::integral_constant&lt; std::decay_t&lt;decltype(f)&gt;, f &gt;; template&lt;auto f&gt; constexpr deleter_t&lt;f&gt; deleter{};现在unique_ptr&lt;T, deleter_t&lt;destroy&gt;&gt; 是您想要的免费删除器。

标签: c++17 c++20 c++ c++17 unique-ptr type-deduction


【解决方案1】:

我建议编写自定义删除器而不是使用函数指针。使用函数指针会使所有unique_ptrs 大小无故增加一倍。

相反,在函数指针上编写一个模板化的删除器:

template <auto deleter_f>
struct Deleter {
    template <typename T>
    void operator()(T* ptr) const
    {
        deleter_f(ptr);
    }
};

或者,如Yakk - Adam Nevraumont mentioned in the comments

template <auto deleter_f>
using Deleter = std::integral_constant<std::decay_t<decltype(deleter_f)>, deleter_f>;

使用它变得非常干净:

auto a = std::unique_ptr<int, Deleter<destroy>>{create()};

尽管您可能希望将此与您的 make_unique_ptr 函数结合使用:

template <auto deleter_f, typename T>
auto create_unique_ptr(T* ptr)
{
    return std::unique_ptr<T, Deleter<deleter_f>>{ptr};
}

// Usage:
auto a = create_unique_ptr<destroy>(create());

【讨论】:

  • “由于删除器是用unique_ptr的类型编码的,所以你的同事不能忘记它。”他们不会只是auto a = make_unqiue_ptr_by_colleague(create(), destroy);吗?
  • @PasserBy 当然,但随后他们的a 将与 OP 编写的函数不兼容,这有望促使同事调查发生了什么
  • 我不认为我明白了。你的意思是当传递给另一个函数时? OP 说这些可能只存在于 RAII。
  • @PasserBy 嗯,好点子。我提出的那一点并没有真正做任何事情
  • 我知道这个 (make_unique_ptr_by_colleague) 是一个很难解决的问题。 Yakk 的回答对此有很大帮助,但我怀疑我可能无法实现它,我可能最终会这样做。非常感谢!
【解决方案2】:

作为值的类型:

template<class T>
struct tag_t {};
template<class T>
constexpr tag_t<T> tag{};

作为类型的值:

template<auto f>
using val_t = std::integral_constant<std::decay_t<decltype(f)>, f>;
template<auto f>
constexpr val_t<f> val{};

请注意,val&lt;some_function&gt; 是一个空类型,可以在带有 ()constexpr 上下文中调用,它将调用 some_function。它还可以存储ints 或其他任何东西,但我们将使用它来无状态地存储函数指针。

现在让我们玩得开心:

namespace factory {
  // This is an ADL helper that takes a tag of a type
  // and returns a function object that can be used
  // to allocate an object of type T.
  template<class T>
  constexpr auto creator( tag_t<T> ) {
    return [](auto&&...args){
      return new T{ decltype(args)(args)... };
    };
  }
  // this is an ADL helper that takes a tag of a type
  // and returns a function object that can be used
  // to destroy that type
  template<class T>
  constexpr auto destroyer( tag_t<T> ) {
    return std::default_delete<T>{};
  }

  // This is a replacement for `std::unique_ptr`
  // that automatically finds the destroying function
  // object using ADL-lookup of `destroyer(tag<T>)`.
  template<class T>
  using unique_ptr = std::unique_ptr< T, decltype(destroyer(tag<T>)) >; // ADL magic here

  // This is a replacement for std::make_unique
  // that uses `creator` and `destroyer` to find
  // function objects to allocate and clean up
  // instances of T.
  template<class T, class...Args>
  unique_ptr<T> make_unique(Args&&...args) {
    // ADL magic here:
    return unique_ptr<T>( creator( tag<T> )(std::forward<Args>(args)...) );
  }
}

好的,这是一个框架。

现在让我们假设您有一些图书馆。它有一个类型。它需要超级秘密的特殊酱料来在这里创建和销毁它的实例:

namespace some_ns {
  struct some_type {
    int x;
  };
  some_type* create( int a, int b ) {
    return new some_type{ a+b }; // ooo secret
  }
  void destroy( some_type* foo ) {
    delete foo; // ooo special
  }
}

我们想把它连接起来。你重新打开命名空间:

namespace some_ns {
  constexpr auto creator( tag_t<some_type> ) {
    return val<create>;
  }
  constexpr auto destoyer( tag_t<some_type> ) {
    return val<destroy>;
  }
}

我们完成了。

factory::unique_ptr&lt;some_ns::some_type&gt; 是将唯一 ptr 存储到 some_type 的正确类型。要创建它,您只需 factory::make_unique&lt;some_ns::some_type&gt;( 7, 2 ) 并获得一个正确类型的唯一 ptr,其中排列着一个销毁器,内存开销为零,并且对销毁器函数的调用不涉及间接。

基本上将std::unique_ptrstd::make_unique 扫描为factory::unique_ptrfactory::make_unique,然后排列创建者/销毁者ADL 助手以使给定类型的所有唯一ptr 做正确的事情。

测试代码:

auto ptr = factory::make_unique<some_ns::some_type>( 2, 3 );
std::cout << ptr->x << "=5\n";
std::cout << sizeof(ptr) << "=" << sizeof(std::unique_ptr<some_ns::some_type>) << "\n";

live example.

与编写自定义唯一 ptr 类型相比,这具有零运行时开销。如果您没有为X 的命名空间(或namespace factory)中的tag_t&lt;X&gt; 重载creatordestroyerfactory::make_unique 将返回一个沼泽标准std::unique_ptr&lt;X&gt;。如果这样做,它会注入编译时信息如何销毁它。

默认情况下factory::make_unique&lt;X&gt; 使用{} 初始化,因此它适用于聚合。

tag_t 系统将启用基于 ADL 的 factory::make_uniquefactory::unique_ptr 自定义。它在其他地方也很有用。最终用户不必知道,他们只需要知道您始终使用factory::unique_ptrfactory::make_unique

搜索std::make_uniquestd::unique_ptr 应该会发现有人违反此规则的情况。最终(你希望)他们会注意到所有唯一指针都是 factory::unique_ptr 而不是 std

向系统添加类型的魔力足够短且易于复制,以至于人们无需了解 ADL 即可完成。我会在某处的 cmets 中包含一个标记为“您不需要知道这一点,但这就是它的工作原理”的段落。

这可能有点过头了;我只是在想如何使用一些新功能处理 中的分布式特征和破坏/创建。我还没有在生产中尝试过,所以可能存在未发现的问题。

【讨论】:

  • 这...实际上可能是可行的。哦,命名空间的魔力:用using factory::unique_ptr 替换一些using std::unique_ptr(并且弃用make_unique_ptr,无论如何我已经这样做了)比尝试找到每个单一用途要简单得多。它仍然有很多工作(定义所有标记的创建者和破坏者)并且确实存在一些问题(当你应该忘记创建标记的破坏者时会导致 UB。哎呀。),所以我会尽我所能。跨度>
  • @cassio 很容易让有一个创造者但没有毁灭者成为一个硬错误。默认创建者只需要返回一个非 lambda;然后在默认销毁器中静态断言创建者是默认的。
【解决方案3】:

您可以做的一件事是使用using 语句为std::unique_ptr&lt;T, void (*)(T*)&gt; 引入别名

template<typename T>
using uptr = std::unique_ptr<T, void (*)(T*)>;

那么你可以像这样使用它

auto u = uptr<int>{create(), &destroy};

【讨论】:

  • 这不也是为了这个目的而引入另一个晦涩的标识符吗?比如,OP 不想要什么?
  • 最好使用不同的自定义点并完全避免这种混乱。
【解决方案4】:

你把事情复杂化了。 Simply specialize std::default_delete for your custom types,你可以使用香草std::unique_ptr

很遗憾std::shared_ptr 没有使用相同的自定义点,您必须明确提供删除器。

【讨论】:

  • 真是巧合。我刚才实际上正在查看链接的答案,因为它弹出在主页上。好东西!实际上,我完全忘记了能够为自定义类型专门化 ::std 类(或者甚至关于 std::default_delete,就此而言)。我想我还有很多东西要学。
  • 话虽如此,不幸的是,我可能没有实现这一点,因为它遇到了与 Yakk 的答案相同的问题。它没有解决类型推导问题(公平地说,所有其他答案也更多地关注删除器而不是推导,这对我来说很好),并且在我应该忘记专门化 std::default_delete 时会导致 UB .我真的不想为了美化某些 RAII 类型而编写 100 多个样板特化。
  • 你知道,预处理器是优秀为你编写这些专业。你只需给它类型和销毁功能。如果你觉得很花哨,还想专门化 make_unique 和 make_shared,也可以添加 create-function。那就是为宏写一次,调用宏的每种类型写一行,取消定义宏的一行。
  • 另外,调用 std::default_delete::operator() 且类型不完整的程序格式不正确。因此,如果库实现者使用它运行,the compiler will complain 如果未提供专业化。
  • 是的,使用 CPP 将是我的方法,但编写这些行并不是真正的问题。没有我可以使用的类型列表:我必须深入研究文档和一些标题才能找到它们。我可能会错过一些,并且必须继续维护这个列表,以防引入新类型。关于您的其他评论:我没明白:当我使用它们时,所有类型都已经完成。还是你的意思是别的?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-03-30
  • 1970-01-01
  • 1970-01-01
  • 2015-04-09
  • 1970-01-01
  • 2015-08-13
  • 2017-05-15
相关资源
最近更新 更多