只要是有良心的 C++ 教材,总是会苦心孤诣地告诫初学者,在继承关系中,应该把父类的析构函数定义成虚函数。这已然成为一条铁律,如果违背,不仅有内存泄漏的风险,在多继承情况下甚至会出现未定义行为。因此,无论在任何情况下,都必须遵循。
然而使用智能指针操作时,似乎会有些有趣的现象。
测试案例std::unique_ptr
:
class B {
public:
~B() {
std::cout << "~Bn";
}
};
class D : public B {
public:
~D() {
std::cout << "~Dn";
}
};
int main() {
B* raw_pointer = new D();
delete raw_pointer;
std::unique_ptr<B> unique_pointer = std::unique_ptr<B>(new D());
unique_pointer = nullptr;
std::unique_ptr<B> unique_pointer_use_make_unique = std::make_unique<D>();
unique_pointer_use_make_unique = nullptr;
return 0;
}
直观分析:继承关系中没有虚析构函数,那毫无疑问,3个指针析构时打印的都是~B
。事实也是如此,MSVC
、GCC
与Clang
的结果与我们的分析一致。
再来看std::shared_ptr
int main() {
std::shared_ptr<B> shared_pointer = std::shared_ptr<B>(new D());
shared_pointer = nullptr;
std::shared_ptr<B> shared_pointer_use_make_shared = std::make_shared<D>();
shared_pointer_use_make_shared = nullptr;
return 0;
}
直观分析:这2个指针析构时打印的都是~B
。然而,三个编译器的行为一致,输出了:
~D
~B
~D
~B
突然就怀疑人生了。显然,析构时调用的是子类的析构函数。而再去调用父类的析构函数是析构子类时默认的行为。
std::shared_ptr
在没有virtual
的情况下居然拥有多态的能力?这当然不是,没有virtual
就没有虚表项,函数表现为静态类型行为。
如果析构函数带上virtual
,使用delete
操作裸指针,即可触发多态。然而,如果直接调用静态类型的析构函数而不是操作裸指针,那有无virtual
是无所谓的。
那为什么std::shared_ptr
表现如此“优秀”?是标准定义的行为?
参考ISO/IEC 14882 C++17
标准文档,23.11.2.2.2 [util.smartptr.shared.dest]
规定了~std::shared_ptr()
的行为:
(1.1) If *this is empty or shares ownership with another shared_ptr instance (use_count() > 1), there are no side effects.
(1.2) — Otherwise, if *this owns an object p and a deleter d, d(p) is called.
(1.3) — Otherwise, *this owns a pointer p, and delete p is called.
很自然的定义,并没有任何奇怪的地方。既然使用了delete
,在没有virtual
的情况下,操作的对象肯定是子类的指针。
分别看看两个构造的MSVC
的实现。
[1] 直接使用 std::shared_ptr 构造:
(这部分涉及沉重的模板推导,不想深究细节可以直接跳到清晰版本)
template <class _Ty>
class shared_ptr : public _Ptr_base<_Ty>
{
// ...
template <class _Ux,
enable_if_t<
conjunction_v<
conditional_t<is_array_v<_Ty>, _Can_array_delete<_Ux>, _Can_scalar_delete<_Ux>>,
_SP_convertible<_Ux, _Ty>>,
int> = 0>
explicit shared_ptr(_Ux* _Px) { // construct shared_ptr object that owns _Px
if constexpr (is_array_v<_Ty>) {
_Setpd(_Px, default_delete<_Ux[]>{});
} else {
_Temporary_owner<_Ux> _Owner(_Px);
_Set_ptr_rep_and_enable_shared(_Owner._Ptr, new _Ref_count<_Ux>(_Owner._Ptr));
_Owner._Ptr = nullptr;
}
}
//...
}
代入std::shared_ptr<B>(new D())
,_Ty -> B
, -Ux -> D
,先分析下可怕的enable_if_t
:
is_array_v<_Ty> -> is_array_v<B> -> false
_Can_array_delete<_Ux> -> _Can_array_delete<D>
_Can_scalar_delete<_Ux> -> _Can_scalar_delete<D>
conditional_t<false, _Can_array_delete<D>, _Can_scalar_delete<D>>
-> _Can_scalar_delete<D>
_SP_convertible<_Ux, _Ty> -> _SP_convertible<D, B>
-> is_convertible<D*, B*>::type
-> integral_constant<bool, true>
conjunction_v<_Can_scalar_delete<D>, integral_constant<bool, true>>
-> true
enable_if_t<true, int> -> int
这个模板构造函数在 SFINAE 的肆虐中生存下来并由编译器相中。模板实例化为
/* template <D, int = 0> */
explicit shared_ptr(D* _Px) {
// if constexpr (is_array_v<_Ty>) {
// _Setpd(_Px, default_delete<_Ux[]>{});
// } else {
_Temporary_owner<D> _Owner(_Px);
_Set_ptr_rep_and_enable_shared(_Owner._Ptr, new _Ref_count<D>(_Owner._Ptr));
_Owner._Ptr = nullptr;
// }
}
所以构造时实际执行了以上未注释的三句。第一句:
template <class _Ux>
struct _Temporary_owner {
_Ux* _Ptr;
explicit _Temporary_owner(_Ux* const _Ptr_) noexcept : _Ptr(_Ptr_) {}
_Temporary_owner(const _Temporary_owner&) = delete;
_Temporary_owner& operator=(const _Temporary_owner&) = delete;
~_Temporary_owner() {
delete _Ptr;
}
};
这个类就是临时保存了指针,所以只剩下中间的一句
// _Px -> new D() -> 临时指针变量
_Set_ptr_rep_and_enable_shared(_Px, new _Ref_count<D>(_Px));
继续跟踪:
template <class _Ux>
void _Set_ptr_rep_and_enable_shared(_Ux* const _Px, _Ref_count_base* const _Rx) noexcept {
this->_Set_ptr_rep(_Px, _Rx);
if constexpr (conjunction_v<negation<is_array<_Ty>>, negation<is_volatile<_Ux>>, _Can_enable_shared<_Ux>>) {
if (_Px && _Px->_Wptr.expired()) {
_Px->_Wptr = shared_ptr<remove_cv_t<_Ux>>(*this, const_cast<remove_cv_t<_Ux>*>(_Px));
}
}
}
先替换
negation<is_array<_Ty>> -> negation<is_array<B>>
negation<is_volatile<_Ux>> -> negation<is_volatile<D>>
_Can_enable_shared<_Ux>> -> _Can_enable_shared<D>>
conjunction_v<negation<is_array<B>>, negation<is_volatile<D>>, _Can_enable_shared<D>>>
-> false
值得一提的,最后那个结构与enable_shared_from_this
有关,即判断D
是否派生于该类。这是另外一个有趣的议题,与 CRTP 有关。
所以模板实例化为:
/* template <class D> */
void _Set_ptr_rep_and_enable_shared(D* const _Px, _Ref_count_base* const _Rx) noexcept {
this->_Set_ptr_rep(_Px, _Rx);
// if constexpr (conjunction_v<negation<is_array<_Ty>>, negation<is_volatile<_Ux>>, _Can_enable_shared<_Ux>>) {
// if (_Px && _Px->_Wptr.expired()) {
// _Px->_Wptr = shared_ptr<remove_cv_t<_Ux>>(*this, const_cast<remove_cv_t<_Ux>*>(_Px));
// }
// }
}
实际这个就是设置了两个数据成员(在父类中定义)
void _Set_ptr_rep(element_type* _Other_ptr, _Ref_count_base* _Other_rep) noexcept { // take new resource
_Ptr = _Other_ptr;
_Rep = _Other_rep;
}
拨开重重的traits工具后,现在这一切看起来十分清晰,std::shared_ptr
在构造函数中做了两件事,初始化了两个数据成员。因为要克服重重障碍,无法直接放在初始化列表中进行。这也就是引用计数与托管对象的分离实现案例。一些有趣的讨论可参考《Effective Modern C++》。
清晰版本:
template <>
class _Ptr_base<B> {
//...
private:
B* _Ptr;
_Ref_count_base* _Rep;
}
template <>
class _Ref_count<D> : public _Ref_count_base {
public:
explicit _Ref_count(D* _Px) : _Ref_count_base(), _Ptr(_Px) {}
private:
// 最后析构函数会调用到这里
virtual void _Destroy() noexcept override {
delete _Ptr;
}
D* _Ptr;
};
template <>
class shared_ptr<B> : public _Ptr_base<B> {
explicit shared_ptr(D* _Px) {
_Ptr = _Px; // _Ptr 静态类型是 B,但保存 new D() 的裸指针
_Rep = new _Ref_count<D>(_Px) // _Ref_count 是 _Ref_count_base 的子类,该类保存了一个指向 D 的指针
}
}
到这一步也许有点眉目了,std::shared_ptr
中保存了裸指针(没错,它的静态类型是B
,但没有virtual
是不能正确析构的),以及一个_Ref_count
结构,它借由模板构造函数窃取到了实际的指针信息,即D
的信息!构造函数结束,operator=
做了移动构造过程,接着看析构函数。它是否如标准定义的那样?
// 析构函数调用链
~shared_ptr() noexcept {
this->_Decref();
}
void _Ptr_base::_Decref() noexcept {
if (_Rep) {
_Rep->_Decref(); // 析构链转入 _Ref_count_base,和父类已经没关系了...
}
}
void _Ref_count_base::_Decref() noexcept {
if (_MT_DECR(_Uses) == 0) {
_Destroy();
_Decwref();
}
}
virtual void _Ref_count::_Destroy() noexcept override {
delete _Ptr; // 这是指向 D 的指针
}
水落石出!所以最后销毁的是子类的指针,即使没有virtual
,但借由模板准确获取到了静态类型。可见虽然标准给了一份定义,但MSVC
却有自己的实现,虽然最后使用的是delete
,但静态类型却是最初new
的类型。不过这种实现也会让人困惑,析构时居然完全不管_Ptr
,只析构引用计数_Rep
,其实原因在构造函数中很清楚,_Ptr
的生命周期由引用计数托管。另外,_Ref_count_base::_Decref()
中还有一个_Decwref();
,这个是判断std::weak_ptr
还在不在的。如果还在的话,虽然指针对象销毁了,引用计数对象_Ref_count
还在的,因为得让std::weak_ptr
查询,如果连std::weak_ptr
都没有了,引用计数就把自己也销毁。
[2] 使用 std::make_shared 构造
由于直接用std::make_shared
会将指针和引用计数捆绑在一起,底层管理与使用new
再包裹std::shared_ptr
有所不同,但构造析构的思路是一致的。
template <class _Ty, class... _Types>
inline shared_ptr<_Ty> make_shared(_Types&&... _Args) {
const auto _Rx = new _Ref_count_obj<_Ty>(_STD forward<_Types>(_Args)...);
shared_ptr<_Ty> _Ret;
_Ret._Set_ptr_rep_and_enable_shared(_Rx->_Getptr(), _Rx);
return _Ret;
}
需要注意的是第一句,因为是用std::make_shared<D>()
构造的,自始至终就没有父类B
的影子。
template <class _Ty>
class _Ref_count_obj : public _Ref_count_base {
public:
template <class... _Types>
explicit _Ref_count_obj(_Types&&... _Args) : _Ref_count_base() {
::new (static_cast<void*>(&_Storage)) _Ty(_STD forward<_Types>(_Args)...);
}
_Ty* _Getptr() { // get pointer
return reinterpret_cast<_Ty*>(&_Storage);
}
private:
virtual void _Destroy() noexcept override {
_Getptr()->~_Ty();
}
aligned_union_t<1, _Ty> _Storage;
};
析构时,调用链最终会调用_Ref_count_obj::_Destroy()
,这个直接调用析构函数,即~D()
,所以仍然是析构静态类型。_Storage
提供了托管对象的空间,用placement new
填充数据。析构时再执行底层指针转换。
标准库中的引用计数如何做在 C++11 之前一直有争论,直到标准制定以及主流编译器实现,才终于尘埃落定。std::shared_ptr
是个有趣的智能指针。由于引用计数的分离,为了正确托管这些对象,不得不添加很多设施来弥补,包括enable_shared_from_this
,比如上面那个_Set_ptr_rep_and_enable_shared()
。两种构造虽然是分开的,但都有_Set_ptr_rep_and_enable_shared
这个函数,殊途同归得到外表一致的std::shared_ptr
。而析构时,又在_Ref_count_base::_Decref()
分道扬镳,指针与引用计数分离的去调用_Ref_count::_Destroy()
,捆绑在一起的去调用_Ref_count_obj::_Destroy()
,但最终都是销毁最初构造的静态类型。然而,std::unique_ptr
的std::make_unique
在内部实际是new
完后用std::unique_ptr
包裹,两种构造是一模一样的。
最后,绝对不能因为std::shared_ptr
的这种实现就抱有侥幸,使用第一种构造时,如果构造前就将指针的静态类型改为父类,那同样会内存泄漏。因此,无论何时,虚析构函数的铁律应严格遵循。