如何避免每次构造或重置时都需要为 std::shared_ptr 指定删除器?
How to avoid the need to specify deleter for std::shared_ptr every time it's constructed or reset?
std::unique_ptr
有两个模板参数,第二个是要使用的删除器。由于这个事实,可以很容易地将 unique_ptr
别名到一个需要自定义删除器(例如 SDL_Texture
)的类型,方法如下:
using SDL_TexturePtr = unique_ptr<SDL_Texture, SDL2PtrDeleter>;
...其中 SDL2PtrDeleter
是用作删除器的函子。
有了这个别名,程序员就可以构造和重置 SDL_TexturePtr
而无需关心甚至不知道自定义删除器:
SDL_TexturePtr ptexture(SDL_CreateTexture(/*args*/));
//...
ptexture.reset(SDL_CreateTexture(/*args*/));
另一方面,std::shared_ptr
没有模板参数,这将允许将删除器指定为类型的一部分,因此以下是非法的:
// error: wrong number of template arguments (2, should be 1)
using SDL_TextureSharedPtr = shared_ptr<SDL_Texture, SDL2PtrDeleter>;
所以,最好的类型别名是:
using SDL_TextureSharedPtr = shared_ptr<SDL_Texture>;
但这与显式使用 shared_ptr<SDL_Texture>
相比没有什么优势,因为用户必须知道要使用的删除函数并在每次构造或重置 SDL_TextureSharedPtr
时指定它:
SDL_TextureSharedPtr ptexture(SDL_CreateTexture(/*args*/), SDL_DestroyTexture);
//...
ptexture.reset(SDL_CreateTexture(/*args*/), SDL_DestroyTexture);
从上面的例子可以看出,用户需要知道删除SDL_Texture
(即SDL_DestroyTexture()
)的正确函数,并且每次都传递一个指向它的指针。除了不方便之外,这还导致程序员通过将不正确的函数指定为删除器而引入错误的可能性很小。
我想以某种方式将删除器封装在共享指针本身的类型中。据我所知,由于没有办法仅通过使用类型别名来实现这一点,因此我考虑了 3 个选项:
创建一个 class,包装 std::shared_ptr<T>
,这将复制 shared_ptr
的接口,但允许通过其自己的模板参数指定删除仿函数。然后,当分别从其自己的构造函数或 reset()
方法调用其底层 std::shared_ptr<T>
实例的构造函数或 reset()
方法时,此包装器将提供指向其删除器实例的 operator()
的指针。当然,缺点是 std::shared_ptr
的整个相当大的界面必须在这个包装 class 中复制,这是 WET。
创建 std::shared_ptr<T>
的子class,这将允许通过其自己的模板参数指定删除仿函数。假设 public
继承,这将帮助我们避免复制 shared_ptr
的界面,但会打开它自己的一堆蠕虫。即使 std::shared_ptr
不是 final
,它似乎并没有被设计为 subclassed,因为它有一个非虚析构函数(尽管这在这个特殊情况)。更糟糕的是,shared_ptr
中的 reset()
方法不是虚拟的,因此不能被覆盖 - 只能被隐藏,这为不正确的使用打开了大门:使用 public
继承,用户可能会通过将我们的 subclass 的实例引用到某些 API,接受 std::shared_ptr<T>&
,其实现可能会调用 reset()
,完全绕过我们的方法。使用非 public 继承,我们得到与选项 #1 相同的结果。
对于以上两个选项,最后,SDL_TextureSharedPtr
可以表示如下,假设MySharedPtr<T, Deleter>
是我们的(sub)class:
using SDL_TextureSharedPtr = MySharedPtr<SDL_Texture, SDL2PtrDeleter>;
- 第三个选项曾经在这里,它涉及专业化
std::default_delete<T>
。这是基于我错误的假设 std::shared_ptr<T>
使用 std::default_delete<T>
,就像 unique_ptr
一样,如果没有明确提供删除器的话。 不是这种情况。感谢 @DieterLücking
指出这一点!
鉴于这些选项和上述推理,这是我的问题。
我是否错过了一种更简单的方法来避免每次构造实例时都必须为 std::shared_ptr<T>
或 reset()
指定删除器?
如果不是,我列出的选项的推理是否正确?是否还有其他 objective 理由让您更喜欢这些选项之一?
您没有将带有大量 using
指令的私有继承作为选项包括在内以公开未更改的功能。
它比使用私有副本时重写共享指针更简单,但允许您编写自定义 reset
而没有暴露的危险。
另请注意,共享指针有一个来自唯一指针的转换构造函数。如果您的工厂函数创建了唯一的指针,则可以根据需要将它们分配给共享指针,并使用正确的删除器。消除代码中的原始指针,重置问题就会消失。
using SDL_TexturePtr = unique_ptr<SDL_Texture, SDL2PtrDeleter>;
Given this alias, programmers are able to construct and reset SDL_TexturePtr
without caring or even knowing about custom deleter:
好吧,这是(通常是致命的)过度简化。而是当且仅当默认构造的删除器适合构造,分别删除器的当前值适合重置指针,而不需要手动更改。
关于包装或扩展 shared_ptr
所发现的缺点,您是对的,尽管有些人可能会说它允许您添加新的实例方法。
不过,您应该尽量减少耦合,这意味着更喜欢自由函数,因为您只需要现有的 public 接口即可编写它们。
如果不指定删除器将导致使用 std::default_delete
(不幸的是它没有)并且每种类型只需要一个删除器,或者标准的 delete-expression 将适合您的用例(好像没有),第三个选项是你可以选择的最好的。
因此,另一种选择:
使用构造函数抽象出(可能复杂的)构造和自定义删除器。
这样你就可以只写一次,自由使用auto
可以进一步减少你的头痛。
您太执着于将删除器放在类型本身中了。而是关注 shared_ptr
实例 的来源。
解决此问题的最有效方法是适当地集中在引入此系统的 shared_ptr
的位置。应该有一个生成它们的函数;它负责附加适当的删除器。
显然这样的系统不提供任何保证。但是,如果您只是从不使用 shared_ptr::reset
(实际上,没有什么理由这样做)并且您从不直接构造一个(copying/moving 可以,但其他构造函数则不行),那么您就是安全的。如果您需要将 shared_ptr
重新分配给新实例,只需使用 operator=
;这就是它的用途。
最终,这与自由使用 make_shared
.
的代码库没有什么不同
您应该可以使用 veneer:
// A shared_ptr which will use SDL2PtrDeleter **by default**:
class SharedTexure : public std::shared_ptr<SDL_Texture> {
public:
constexpr SharedTexure() : std::shared_ptr<SDL_Texture>() {}
constexpr SharedTexure(std::nullptr_t) : std::shared_ptr<SDL_Texture>() {}
explicit SharedTexure(SDL_Texture* texture) :
std::shared_ptr<SDL_Texture>(texture, SDL2PtrDeleter()) {}
SharedTexture(std::shared_ptr<SDL_Texture> texture) :
std::shared_ptr<SDL_Texture>(std::move(texture)) {}
};
可以概括为:
template<class T, class D>
class shared_ptr : public std::shared_ptr<T> {
public:
constexpr shared_ptr() : std::shared_ptr<T>() {}
constexpr shared_ptr(std::nullptr_t) : std::shared_ptr<T>() {}
template<class U>
explicit shared_ptr(U* ptr) :
std::shared_ptr<T>(ptr, D()) {}
template<class U>
shared_ptr(std::shared_ptr<U> ptr) :
std::shared_ptr<T>(std::move(ptr)) {}
};
using SharedTexure = shared_ptr<SDL_Texture, SDL2PtrDeleter>;
您应该能够继承构造函数:
template<class T, class D>
class shared_ptr : public std::shared_ptr<T> {
public:
using std::shared_ptr<T>::shared_ptr;
template<class U>
explicit shared_ptr(U* ptr) :
std::shared_ptr<T>(ptr, D()) {}
};
it doesn't seem to have been designed to be subclassed,
since it has a non-virtual destructor
referenced paper 暗示它对于这个用例是安全的。但是,从标准中获得合适的参考资料会很有趣。
只需将 unique_ptr
移动到 shared_ptr
。 shared_ptr 将继承自定义删除器。请参阅 code(抱歉正在尝试使用 cJSON 库,但您会明白这一点):
#include <memory>
#include <iostream>
struct cJSON { } obj;
extern "C" cJSON* cJSON_ParseWithLength() { return &obj; }
void cJSON_Delete(cJSON* item)
{
std::cout << "custom deleter used" << std::endl;
}
auto factory()
{
return std::unique_ptr<cJSON, decltype(&cJSON_Delete)>{ cJSON_ParseWithLength(), cJSON_Delete };
}
int main()
{
auto uptr = factory();
std::shared_ptr sptr{std::move(uptr)};
sptr.reset();
std::cout << "now we're done" << std::endl;
}
输出:
custom deleter used
now we're done
std::unique_ptr
有两个模板参数,第二个是要使用的删除器。由于这个事实,可以很容易地将 unique_ptr
别名到一个需要自定义删除器(例如 SDL_Texture
)的类型,方法如下:
using SDL_TexturePtr = unique_ptr<SDL_Texture, SDL2PtrDeleter>;
...其中 SDL2PtrDeleter
是用作删除器的函子。
有了这个别名,程序员就可以构造和重置 SDL_TexturePtr
而无需关心甚至不知道自定义删除器:
SDL_TexturePtr ptexture(SDL_CreateTexture(/*args*/));
//...
ptexture.reset(SDL_CreateTexture(/*args*/));
另一方面,
std::shared_ptr
没有模板参数,这将允许将删除器指定为类型的一部分,因此以下是非法的:
// error: wrong number of template arguments (2, should be 1)
using SDL_TextureSharedPtr = shared_ptr<SDL_Texture, SDL2PtrDeleter>;
所以,最好的类型别名是:
using SDL_TextureSharedPtr = shared_ptr<SDL_Texture>;
但这与显式使用 shared_ptr<SDL_Texture>
相比没有什么优势,因为用户必须知道要使用的删除函数并在每次构造或重置 SDL_TextureSharedPtr
时指定它:
SDL_TextureSharedPtr ptexture(SDL_CreateTexture(/*args*/), SDL_DestroyTexture);
//...
ptexture.reset(SDL_CreateTexture(/*args*/), SDL_DestroyTexture);
从上面的例子可以看出,用户需要知道删除SDL_Texture
(即SDL_DestroyTexture()
)的正确函数,并且每次都传递一个指向它的指针。除了不方便之外,这还导致程序员通过将不正确的函数指定为删除器而引入错误的可能性很小。
我想以某种方式将删除器封装在共享指针本身的类型中。据我所知,由于没有办法仅通过使用类型别名来实现这一点,因此我考虑了 3 个选项:
创建一个 class,包装
std::shared_ptr<T>
,这将复制shared_ptr
的接口,但允许通过其自己的模板参数指定删除仿函数。然后,当分别从其自己的构造函数或reset()
方法调用其底层std::shared_ptr<T>
实例的构造函数或reset()
方法时,此包装器将提供指向其删除器实例的operator()
的指针。当然,缺点是std::shared_ptr
的整个相当大的界面必须在这个包装 class 中复制,这是 WET。创建
std::shared_ptr<T>
的子class,这将允许通过其自己的模板参数指定删除仿函数。假设public
继承,这将帮助我们避免复制shared_ptr
的界面,但会打开它自己的一堆蠕虫。即使std::shared_ptr
不是final
,它似乎并没有被设计为 subclassed,因为它有一个非虚析构函数(尽管这在这个特殊情况)。更糟糕的是,shared_ptr
中的reset()
方法不是虚拟的,因此不能被覆盖 - 只能被隐藏,这为不正确的使用打开了大门:使用public
继承,用户可能会通过将我们的 subclass 的实例引用到某些 API,接受std::shared_ptr<T>&
,其实现可能会调用reset()
,完全绕过我们的方法。使用非 public 继承,我们得到与选项 #1 相同的结果。
对于以上两个选项,最后,SDL_TextureSharedPtr
可以表示如下,假设MySharedPtr<T, Deleter>
是我们的(sub)class:
using SDL_TextureSharedPtr = MySharedPtr<SDL_Texture, SDL2PtrDeleter>;
- 第三个选项曾经在这里,它涉及专业化
std::default_delete<T>
。这是基于我错误的假设std::shared_ptr<T>
使用std::default_delete<T>
,就像unique_ptr
一样,如果没有明确提供删除器的话。 不是这种情况。感谢@DieterLücking
指出这一点!
鉴于这些选项和上述推理,这是我的问题。
我是否错过了一种更简单的方法来避免每次构造实例时都必须为 std::shared_ptr<T>
或 reset()
指定删除器?
如果不是,我列出的选项的推理是否正确?是否还有其他 objective 理由让您更喜欢这些选项之一?
您没有将带有大量 using
指令的私有继承作为选项包括在内以公开未更改的功能。
它比使用私有副本时重写共享指针更简单,但允许您编写自定义 reset
而没有暴露的危险。
另请注意,共享指针有一个来自唯一指针的转换构造函数。如果您的工厂函数创建了唯一的指针,则可以根据需要将它们分配给共享指针,并使用正确的删除器。消除代码中的原始指针,重置问题就会消失。
using SDL_TexturePtr = unique_ptr<SDL_Texture, SDL2PtrDeleter>;
Given this alias, programmers are able to construct and reset
SDL_TexturePtr
without caring or even knowing about custom deleter:
好吧,这是(通常是致命的)过度简化。而是当且仅当默认构造的删除器适合构造,分别删除器的当前值适合重置指针,而不需要手动更改。
关于包装或扩展 shared_ptr
所发现的缺点,您是对的,尽管有些人可能会说它允许您添加新的实例方法。
不过,您应该尽量减少耦合,这意味着更喜欢自由函数,因为您只需要现有的 public 接口即可编写它们。
如果不指定删除器将导致使用 std::default_delete
(不幸的是它没有)并且每种类型只需要一个删除器,或者标准的 delete-expression 将适合您的用例(好像没有),第三个选项是你可以选择的最好的。
因此,另一种选择:
使用构造函数抽象出(可能复杂的)构造和自定义删除器。
这样你就可以只写一次,自由使用auto
可以进一步减少你的头痛。
您太执着于将删除器放在类型本身中了。而是关注 shared_ptr
实例 的来源。
解决此问题的最有效方法是适当地集中在引入此系统的 shared_ptr
的位置。应该有一个生成它们的函数;它负责附加适当的删除器。
显然这样的系统不提供任何保证。但是,如果您只是从不使用 shared_ptr::reset
(实际上,没有什么理由这样做)并且您从不直接构造一个(copying/moving 可以,但其他构造函数则不行),那么您就是安全的。如果您需要将 shared_ptr
重新分配给新实例,只需使用 operator=
;这就是它的用途。
最终,这与自由使用 make_shared
.
您应该可以使用 veneer:
// A shared_ptr which will use SDL2PtrDeleter **by default**:
class SharedTexure : public std::shared_ptr<SDL_Texture> {
public:
constexpr SharedTexure() : std::shared_ptr<SDL_Texture>() {}
constexpr SharedTexure(std::nullptr_t) : std::shared_ptr<SDL_Texture>() {}
explicit SharedTexure(SDL_Texture* texture) :
std::shared_ptr<SDL_Texture>(texture, SDL2PtrDeleter()) {}
SharedTexture(std::shared_ptr<SDL_Texture> texture) :
std::shared_ptr<SDL_Texture>(std::move(texture)) {}
};
可以概括为:
template<class T, class D>
class shared_ptr : public std::shared_ptr<T> {
public:
constexpr shared_ptr() : std::shared_ptr<T>() {}
constexpr shared_ptr(std::nullptr_t) : std::shared_ptr<T>() {}
template<class U>
explicit shared_ptr(U* ptr) :
std::shared_ptr<T>(ptr, D()) {}
template<class U>
shared_ptr(std::shared_ptr<U> ptr) :
std::shared_ptr<T>(std::move(ptr)) {}
};
using SharedTexure = shared_ptr<SDL_Texture, SDL2PtrDeleter>;
您应该能够继承构造函数:
template<class T, class D>
class shared_ptr : public std::shared_ptr<T> {
public:
using std::shared_ptr<T>::shared_ptr;
template<class U>
explicit shared_ptr(U* ptr) :
std::shared_ptr<T>(ptr, D()) {}
};
it doesn't seem to have been designed to be subclassed, since it has a non-virtual destructor
referenced paper 暗示它对于这个用例是安全的。但是,从标准中获得合适的参考资料会很有趣。
只需将 unique_ptr
移动到 shared_ptr
。 shared_ptr 将继承自定义删除器。请参阅 code(抱歉正在尝试使用 cJSON 库,但您会明白这一点):
#include <memory>
#include <iostream>
struct cJSON { } obj;
extern "C" cJSON* cJSON_ParseWithLength() { return &obj; }
void cJSON_Delete(cJSON* item)
{
std::cout << "custom deleter used" << std::endl;
}
auto factory()
{
return std::unique_ptr<cJSON, decltype(&cJSON_Delete)>{ cJSON_ParseWithLength(), cJSON_Delete };
}
int main()
{
auto uptr = factory();
std::shared_ptr sptr{std::move(uptr)};
sptr.reset();
std::cout << "now we're done" << std::endl;
}
输出:
custom deleter used
now we're done