std::shared_ptrs的自定义删除器

7

在不使用new的情况下创建std::shared_ptr后,是否可以使用自定义删除器?

我的问题是对象的创建由工厂类处理,其构造函数和析构函数受保护,这会导致编译错误,并且我不想使用new,因为它有缺点。

详细说明:我喜欢像这样创建共享指针,两者都没有让您设置自定义删除器(我认为):

auto sp1 = make_shared<Song>(L"The Beatles", L"Im Happy Just to Dance With You");

或者我可以像这样创建它们,这样就可以通过参数设置删除器:
auto sp2(new Song, MyDeleterFunc);

但第二种使用new的方式,据我所知,不如顶部的分配方式高效。

也许这样更清楚:是否可能同时获得make_shared<>的好处和自定义删除器?这是否意味着必须编写一个分配器?


1
你能否添加一些(伪)代码以解释你需要什么或者进行进一步的阐述吗? (我怀疑这是一个 XY 问题) - Daniel Frey
我不明白你的问题,当然你可以自由地使用你的值初始化shared_ptr,它不一定是new运算符返回的东西。 - Pavel Beliy
请详细说明一下,我对智能指针还不熟悉。 - Kristian D'Amato
在使用共享指针时,几乎没有必要保护析构函数,除非有极为特殊的情况。请克制自己,不要将类保护得过于严密,以至于连 delete sptr.get() 这样的恶意代码都无法执行。 - Fozi
嗯...一旦你使用了“protected”保护了你的构造函数,那种冲动就很难控制了,至少对我来说是这样! - Kristian D'Amato
显示剩余2条评论
2个回答

8

不,没有任何形式的std::make_shared支持自定义删除器。

如果需要返回一个带有自定义删除器的shared_ptr,则必须承担性能损失。

想一想:如果使用make_shared,它将分配一个更大的内存区域,可以同时存储引用计数和您的对象,并且会调用放置的新内容。从make_shared返回的shared_ptr已经具有自定义删除器,该删除器显式地调用您的对象的析构函数,然后释放更大的内存块。


4

在您的情况下,您需要使用new,因为std::make_shared的设计只有在std::make_shared可以使用其自己的(内部)自定义删除器来释放对象和shared_count的组合内存时才能起作用,从而避免额外的分配。

您必须接受,使用自定义删除器时无法优化分配,但仍应该使用类似于std::make_shared的封装器来封装new,以确保安全使用。这有助于避免在对象的构造函数抛出异常并且某人使用时发生内存泄漏的情况。

template<typename T>
void f(const T&, const T&);

f( std::shared_ptr<X>(new X), std::shared_ptr<X>(new X) ); // possible leak

替代

std::shared_ptr<X> make_X() { return std::shared_ptr<X>(new X); }
f( make_X(), make_X() ); // no leak

谢谢。你能详细解释一下为什么第二个比第一个更好吗?我不明白为什么只有第一个存在潜在的泄漏问题。 - Kristian D'Amato
2
在第二种情况下,每次调用 make_X 都会确保从 new 返回的每个指针都立即存储在 std::shared_ptr 中,因此即使出现异常,也有一个 shared_ptr 将删除从 new 返回的实例。 - Daniel Frey
有没有什么合理的东西?可以像make_shared一样工作,并且可以与非公共析构函数一起使用?在我看来,需要具有公共析构函数的要求破坏了拥有共享指针的整个目的。 - uuu777
这种方法有什么问题:make_shared<storage_with_deleter<T, D>>,其中storage_with_deleter<T, D>包含一个std::aligned_storage_t<sizeof(T),alignof(T)> t;和一个D d;,在构造时将t放置到内存中,并在~storage_with_deleter中调用d(static_cast<T*>(&t)),然后使用别名构造函数将其转换为shared_ptr<T> - Yakk - Adam Nevraumont
@Yakk 它可能起作用,但由于make_shared的一个目标是更高效,通过别名构造函数返回另一个shared_ptr实例将防止省略或其他优化(取决于上下文),需要额外的原子引用计数器添加和删除。最终您必须对其进行基准测试,以查看是否真正值得额外的复杂性。 - Daniel Frey
@DanielFrey 说得好:别名构造函数采用const&而不是值(或右值引用)传递。这看起来像是一个缺陷,因为它强制执行原子增量和减量。然而,我怀疑两个原子操作是否会比单个额外的内存分配加上所有后来的缓存未命中更昂贵。我认为编写make_shared_with_deleter应该可以解决问题,但我不确定要使用什么语法...使用转发元组很麻烦。 - Yakk - Adam Nevraumont

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接