我当前正在使用一个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介绍了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版本上确实有效,但是该标准不太喜欢它:
[namespace.std]
1除非另有说明,否则如果C ++程序将声明或定义添加到名称空间std或名称空间std中的名称空间中,则行为是不确定的。
还有一些与区分指针和数组有关的问题,但让我们忽略它。
最后,问题:在使用自定义删除器时,是否还有其他方法可以“帮助” std::unique_ptr
(或也许std::make_unique
)来推断正确的类型?以防万一这是XY问题,对于这些类型的生命周期管理(也许std::shared_ptr
),我是否没有想到过任何解决方案?如果这两个答案均否定,那么我应该期待c++20上有什么改进可以解决此问题?
答案 0 :(得分: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<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,该ptr具有排成一行的销毁器,内存开销为零,并且对销毁器函数的调用不涉及任何间接操作。
基本上清除std::unique_ptr
和std::make_unique
的{{1}}和factory::unique_ptr
,然后排列创建者/销毁者ADL助手以使所有唯一的ptrs成为给定类型,只需执行正确的事情。
测试代码:
factory::make_unique
与编写自定义唯一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";
的命名空间(或creator
中)为destroyer
重载tag_t<X>
或X
,则namespace factory
返回一个沼泽标准factory::make_unique
。如果这样做,它将向编译时信息注入如何销毁它的信息。
默认情况下,std::unique_ptr<X>
使用factory::make_unique<X>
初始化,以便与聚合一起使用。
{}
系统将启用基于tag_t
和factory::make_unique
的基于ADL的自定义。它在其他地方也很有用。最终用户不必了解它,他们只需要知道您始终使用factory::unique_ptr
和factory::unique_ptr
。
搜索factory::make_unique
和std::make_unique
应该会发现您有人违反此规则的情况。最终(您希望)他们会注意到所有唯一指针都是std::unique_ptr
而不是factory::unique_ptr
。
向系统添加类型的魔力足够短且易于复制,人们无需了解ADL就可以做到。我会在某处的注释中添加一段标记为“您不需要知道这一点,但这就是它的工作原理”的段落。
这可能太过分了;我只是在考虑如何使用一些新功能来处理c++17中的分布式特征和破坏/创建。我尚未在生产中尝试此操作,因此可能存在未公开的问题。
答案 1 :(得分:1)
您可以做的一件事是使用using
语句为std::unique_ptr<T, void (*)(T*)>
引入别名,例如
template<typename T>
using uptr = std::unique_ptr<T, void (*)(T*)>;
然后您可以像使用它
auto u = uptr<int>{create(), &destroy};
答案 2 :(得分:1)
我建议编写自定义删除器,而不要使用函数指针。使用函数指针会毫无疑问使所有unique_ptr
的大小加倍。
相反,编写一个以函数指针为模板的删除器:
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());
答案 3 :(得分:0)
您使事情变得过于复杂。 Simply specialize std::default_delete
for your custom types,您可以使用香草std::unique_ptr
。
std::shared_ptr
没有使用相同的自定义点,这很可惜,您将不得不显式提供删除程序。