夜雨聆风学习资料网

ARTICLE · 1247863

libcxx stl源码分析-vector的resize函数

libcxx stl源码分析-vector的resize函数
在libcxx stl源码分析-容器vector分析(2)libcxx stl源码分析-容器vector分析(2)中 我们以如下例子
class A {public:  A(int) {    std::cout << "A(int)\n";  }  A() {    std::cout << "A()\n";  };  ~A() {    std::cout << "~A()\n";  };  intmain(){    std::vector<A> a{5};    a.resize(10);    a.resize(2);    return 0;}
讲解了a.resize(10) 的工作原理。下面我们来讲解下a.resize(2)的行为。
在标准库中vector的resize函数实现如下
template <class_Tp, class_Allocator>void vector<_Tp, _Allocator>::resize(size_type __new_size) {  size_type __current_size = size();  if (__current_size < __new_size) {    if (__new_size <= capacity()) {      __construct_at_end(__new_size - __current_size);    } else {      _SplitBuffer __v(__recommend(__new_size), __current_size, __layout_.__alloc());      __v.__construct_at_end(__new_size - __current_size);      __layout_.__relocate(__v);    }  } else if (__current_size > __new_size) {    this->__destruct_at_end(this->__layout_.__begin_ptr() + __new_size);  }}
当a.resize(10)执行结束后,上述函数中__current_size = 10; 此时,__new_size= 2;
所以上述函数会命中如下实现
    this->__destruct_at_end(this->__layout_.__begin_ptr() + __new_size);
上述__destruct_at_end函数实现如下
 void __destruct_at_end(pointer __new_last) _NOEXCEPT {    size_type __old_size = size();    std::__allocator_destroy(__layout_.__alloc(), __new_last, __layout_.__end_ptr());    __layout_.__set_bound_using_pointer(__new_last);    __annotate_shrink(__old_size);  }
这段是 libc++vector里的__destruct_at_end,作用是把容器的有效区间从尾部收缩到__new_last,销毁被裁掉的那段对象。它只负责"销毁对象",不释放内存——延续上一轮__allocator_destroy的职责划分。

我们主要讲解下这个函数

std::__allocator_destroy(__layout_.__alloc(), __new_last, __layout_.__end_ptr());
上述函数最终会调用到如下两个destroy函数,改函数实现如下
template <class _Tp, class =        __enable_if_t<__has_destroy<allocator_type, _Tp*>::value> >    static void destroy(allocator_type& __a, _Tp* __p) {      _LIBCPP_SUPPRESS_DEPRECATED_PUSH            __a.destroy(__p);       _LIBCPP_SUPPRESS_DEPRECATED_POP           }    template <class _Tp, class = void, class =        __enable_if_t<!__has_destroy<allocator_type, _Tp*>::value> >    static void destroy(allocator_type&, _Tp* __p) {#if _LIBCPP_STD_VER > 17        _VSTD::destroy_at(__p);#else        __p->~_Tp();#endif    }

这段是allocator_traits里destroy的两个重载,核心是用 SFINAE 在编译期选择"调用 allocator 自己的destroy"还是"直接调用析构函数"。这是allocator_traits为兼容旧式 allocator 而设计的经典检测机制。

准 allocator 模型里,destroy有两条路径:

allocator 自己提供了destroy成员(C++17 前的旧式 allocator 可能有)→ 应该调用__a.destroy(__p),尊重用户的定制。

allocator 没有提供destroy(C++17 后的 allocator 通常只有allocate/deallocate,construct/destroy已被弃用/移除)→ 直接调用p->~T()(或 C++17 后的destroy_at)。

两个重载通过__enable_if_t互斥地启用,实现编译期二选一。

第一个重载:allocator 有 destroy

启用条件:__has_destroy::value为真——即 allocator 确实有可调用的destroy(_Tp*)成员。

技巧:用第二个模板参数的默认值做 SFINAE

template <class _Tp, class = __enable_if_t<条件>>

第二个模板参数class没有名字,其默认值是__enable_if_t<条件>。

当条件为真时,__enable_if_t<...>是void(或其他类型),默认参数合法,重载可用。

当条件为假时,__enable_if_t<...>触发替换失败(SFINAE),该重载从候选集中被静默移除,不报错。

这样两个重载就能互斥启用,且不需要给模板参数命名(因为只是"开关",不用它)。

函数体:

_LIBCPP_SUPPRESS_DEPRECATED_PUSH__a.destroy(__p);_LIBCPP_SUPPRESS_DEPRECATED_POP

调用 allocator 的destroy成员。

外面包了_LIBCPP_SUPPRESS_DEPRECATED_PUSH/POP:因为 C++17 起allocator::destroy被标记为弃用(deprecated),libc++ 内部仍要调用它(为兼容旧 allocator),所以临时压掉弃用告警,避免内部实现自己触发警告。

注释性的PUSH/POP对应编译器 pragma(如#pragmaclang diagnostic push/pop),作用域内抑制-Wdeprecated-declarations。

_LIBCPP_CONSTEXPR_AFTER_CXX17:C++17 起该函数可为constexpr(C++20 起std::allocator::destroy被移除,但allocator_traits::destroy仍是 constexpr 能力)。因为析构在常量求值中受限制,这个宏是 libc++ 常见的"条件 constexpr"处理。

第二个重载:allocator 没有 destroy

启用条件:!__has_destroy<...>::value——allocator没有destroy成员。

为什么多一个class = void:
template <class _Tp, class = void, class = __enable_if_t<条件>>

第一个重载的模板参数列表是>(两个参数)。

第二个是>(三个参数)。

两者参数个数不同,是为了避免在某些情况下两个重载的签名"看起来相同"导致歧义/重定义。由于条件 A、B 互斥,实际永远只有一个可用,但模板参数列表的形态差异进一步保证它们不是同一模板的重复声明。

中间的class = void是占位,使第二个__enable_if_t落在第三个位置。

函数体:
#if _LIBCPP_STD_VER > 17    _VSTD::destroy_at(__p);#else    __p->~_Tp();#endif

C++17 之后:调用std::destroy_at(__p)。destroy_at是 C++17 引入的,语义是"对p指向的对象调用析构",且对数组类型有特殊处理(逐元素析构)。它比p->~T()更通用、更符合现代写法。

C++17 之前:没有destroy_at,直接__p->~_Tp()显式调用析构函数。

参数allocator_type&无名——因为这条路径不需要 allocator,只是为保持与第一个重载相同的调用接口(allocator_traits::destroy(a, p)总是传 allocator,但这里用不上)。

总结
回到最开始,当new_size < current_size时,vector::resize只做"截断":
销毁[begin + new_size, end)的尾部元素,把end前移到begin + new_size,不移动、不构造、不重新分配、不释放内存;size()变为new_size,capacity()与begin()不变,前缀元素的值和地址完全保持原样,缓冲区内存保留供后续复用。

对resize(更小)而言:

销毁被裁掉的对象:对[begin + __new_size, 原 end)这段共__current_size - __new_size个元素逐个调用析构(通过allocator_traits::destroy,尊重 allocator 定制)。

end前移:end = begin + __new_size,有效区间变成[begin, begin + __new_size)。

sanitizer 注解:更新容器的可访问范围。

相关学习资料