[英]Using automatic deduction with unique_ptr and custom deleter
我目前正在使用一个定义了许多数据类型的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);
这看起来很不错,但是引入了一个非标准函数,除了作为某些声明的语法糖之外没有任何实际用途。 我的同事可能甚至不知道它存在,最终用不同的名称创建它的其他版本。
c ++ 17引入了类模板参数推导 。 我认为这是我的问题的完美解决方案:一种标准的方法来推导所有这些类型而不诉诸用户定义的包装器。 所以我尝试了这个:
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版本上有效,但标准不太喜欢它:
[namespace.std]
1除非另有说明,否则如果C ++程序向名称空间std或命名空间std中的命名空间添加声明或定义,则其行为是未定义的。
还有一些与区分指针和数组有关的问题,但让我们忽略它。
最后,问题:在使用自定义删除器时,有没有其他方法可以'帮助' std::unique_ptr
(或者std::make_unique
)来推断出正确的类型? 万一这是一个XY问题,是否有任何解决方案我没有想到这些类型的生命周期管理(也许是std::shared_ptr
)? 如果这两个答案都是否定的,那么对于我应该期待的c ++ 20是否有任何改进,那么解决这个问题呢?
作为值的类型:
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<some_function>
是一个空类型,可以使用()
在constexpr
上下文中调用,它将调用some_function
。 它也可以存储int
或者其他什么,但是我们将使用它来无状态地存储函数指针。
现在让我们玩得开心:
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<some_ns::some_type>
是将唯一ptr存储到some_type
的正确类型。 要创建它你只需要factory::make_unique<some_ns::some_type>( 7, 2 )
并获得一个正确类型的唯一ptr,其中一个驱逐舰排成factory::make_unique<some_ns::some_type>( 7, 2 )
,内存开销为零,并且对驱逐舰功能的调用涉及否间接。
基本上为factory::unique_ptr
和factory::make_unique
扫描std::unique_ptr
和std::make_unique
,然后将创建者/驱逐舰ADL助手排成一行,使所有唯一的ptrs到达给定类型,做正确的事情。
测试代码:
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";
实例 。
与编写自定义唯一ptr类型相比,这没有运行时开销。 如果你没有为X
的命名空间(或namespace factory
)中的tag_t<X>
重载creator
或destroyer
creator
,那么factory::make_unique
将返回一个标准的标准std::unique_ptr<X>
。 如果这样做,它会注入编译时信息如何销毁它。
默认情况下, factory::make_unique<X>
使用{}
初始化,因此它适用于聚合。
tag_t
系统用于启用基于ADL的factory::make_unique
和factory::unique_ptr
自定义。 它在其他地方也很有用。 最终用户不必了解它,他们只需要知道你总是使用factory::unique_ptr
和factory::make_unique
。
搜索std::make_unique
和std::unique_ptr
发现有人违反此规则的情况。 最终(你希望)他们会注意到所有唯一的指针都是factory::unique_ptr
而不是std
。
向系统添加类型的魔力足够短,并且很容易复制,人们可以在不必了解ADL的情况下进行复制。 我会在某处的评论中加入一段标有“你不需要知道这个,但这是如何工作”的段落。
这可能过头了; 我正在考虑如何使用一些新功能在c ++ 17中处理分布式特征和破坏/创建。 我没有在生产中试过这个,所以可能会有一些未解决的问题。
您可以做的一件事是使用using
语句为std::unique_ptr<T, void (*)(T*)>
引入别名
template<typename T>
using uptr = std::unique_ptr<T, void (*)(T*)>;
然后你可以像使用它一样
auto u = uptr<int>{create(), &destroy};
我建议编写自定义删除器而不是使用函数指针。 使用函数指针会使所有unique_ptr
的大小无缘无故。
相反,写一个模板化函数指针的删除器:
template <auto deleter_f>
struct Deleter {
template <typename T>
void operator()(T* ptr) const
{
deleter_f(ptr);
}
};
或者,正如Yakk - Adam Nevraumont在评论中提到的那样 :
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());
你太复杂了。 只需将std::default_delete
专门用于自定义类型 ,就可以使用vanilla std::unique_ptr
。
遗憾的是std::shared_ptr
不使用相同的自定义点,你必须明确地提供删除器。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.