C++ std::vector和移动优化

vector的内存模型

STL的vector容器是最经典也是使用频率最高的顺序容器,vector采用了连续内存模型,是对动态数组的封装。vector具有随机索引,高效访问的特性,在容器末尾添加元素(push_back)也具有O(1)的均摊复杂度。但是在中间位置插入元素以及自动扩容会有较大的性能开销。

vector的值语义

Stl的容器基本都是值语义,即容器中存储的都是对象的副本,所以往容器中添加对象的时候需要拷贝源对象,可以分为三种情况:

  • 在容器尾部正常插入元素 push_back,需要调用拷贝构造复制对象
  • 在容器中间插入元素 insert,需要将插入位置后面的元素往后移动,此时对于Pod类型,可以直接进行memory copy,将整块内存往后拷贝,对于非Pod类型,则需要挨个对象拷贝
  • 插入元素后需要扩容 ,重新分配内存,将原来位置的对象复制到新的内存中,并销毁原来内存

vector应该尽量避免在中间位置插入元素,并且在能预知大小的情况下,使用reserve函数预留空间,避免频繁地重新分配内存。

移动语义优化

添加元素

C++引入的移动语义,让vector的拷贝得以优化,已有对象可以重复使用,而无需频繁拷贝构造。vector中添加一个元素有三种途径:

  • 直接在内存中通过拷贝构造函数构造对象
  • 直接在内存中通过构造函数构造对象
  • 直接在内存中通过移动构造函数构造对象

在容器中构造对象调用的是placement new。

push_back

push_back提供了左值和右值两个版本的重载,内部实现直接调用的emplace_back,实参可为如下情况

  • 实参为左值对象
  • 实参为右值对象
  • 实参为单参数的构造函数的参数,隐式转换为对象(单参构造函数无explicit)
void push_back(const _Ty& _Val) { // insert element at end, provide strong guarantee
    emplace_back(_Val);
}
void push_back(_Ty&& _Val) {
    // insert by moving into element at end, provide strong guarantee
    emplace_back(_STD move(_Val));
}

emplace_back

emplace_back函数是形参为万能引用的模板函数,内部通过forwar完美转发将左值右值转发到相应的重载函数上,在容器中构造对象时分为如下情况:

  • 实参为左值对象,调用拷贝构造函数
  • 实参为右值对象,调用移动构造函数,若无移动构造,调用拷贝构造函数(拷贝构造函数形参为const&,可接收右值作为参数)
  • 实参为构造函数的参数,调用相应参数的构造函数
template <class... _Valty>
decltype(auto) emplace_back(_Valty&&... _Val) {
    // insert by perfectly forwarding into element at end, provide strong guarantee
    auto& _My_data   = _Mypair._Myval2;
    pointer& _Mylast = _My_data._Mylast;
    if (_Mylast != _My_data._Myend) {
        return _Emplace_back_with_unused_capacity(std::forward<_Valty>(_Val)...);
    }
    _Ty& _Result = *_Emplace_reallocate(_Mylast, std::forward<_Valty>(_Val)...);
    return _Result;
}

✍️右值优先匹配到移动构造函数,如果无移动构造函数,则匹配到拷贝构造函数,能移则移,反之则拷

✍️Stl容器中,emplace开头的函数一般有右值优化,能提供较高的效率

vector扩容

移动语义下,vector扩容时就有了优化的空间,毕竟原来对象的资源是可以重复使用的,vector也确实使用移动构造来代替拷贝构造,但是这种优化是有限制的,即vector中存储对象必须有移动构造函数,而且需要是noexcept的。

class Object
{
public:
    Object(Object&& obj) noexcept;
};
vector<Object> vec;// 扩容时使用移动构造

因为移动构造会损坏源对象,如果对象移动的过程中发生了异常,那么原来的vector的数据就处于损坏的状态,而且不可恢复,为了保证异常安全,因而才有noexception的限制。

对于左值,直接用拷贝构造函数构造在容器,对于右值,直接用移动构造构造在容器,只需要调用一次构造函数

移动优化实现探究

在vector的扩容重新分配内存中,有一段代码决定了是否能够使用移动构造函数,其中的关键是is_nothrow_move_constructible,此类是定义在type_traits中的用于判断是否有异常安全的移动构造函数

if constexpr (is_nothrow_move_constructible_v<_Ty> || !is_copy_constructible_v<_Ty>) {
    _Uninitialized_move(_Myfirst, _Mylast, _Newvec, _Al);
} else {
    _Uninitialized_copy(_Myfirst, _Mylast, _Newvec, _Al);
}

is_nothrow_move_constructible的实现稍微有点复杂,但是也不至于难懂,其中运用的正是Sfiane技巧,那么编译器是怎么知道构造函数有noexcept修饰符呢? 我一度以为是编译器在内部加了什么戏份,最后看了源代码,发现很简单,那就是noexcept,noexcept有两种语义:

  • noexcept修饰符(specifier),用于标识函数不抛出异常
  • noexcept操作符(operator),用于检查表达式是否抛出异常
template <bool, bool, class _Tp, class... _Args>
struct __libcpp_is_nothrow_constructible;

template <class _Tp, class... _Args>
struct __libcpp_is_nothrow_constructible</*is constructible*/ true, /*is reference*/ false, _Tp, _Args...>
    : public integral_constant<bool, noexcept(_Tp(std::declval<_Args>()...))> {};

template <class _Tp>
void __implicit_conversion_to(_Tp) noexcept {}

template <class _Tp, class _Arg>
struct __libcpp_is_nothrow_constructible</*is constructible*/ true, /*is reference*/ true, _Tp, _Arg>
    : public integral_constant<bool, noexcept(std::__implicit_conversion_to<_Tp>(std::declval<_Arg>()))> {};

template <class _Tp, bool _IsReference, class... _Args>
struct __libcpp_is_nothrow_constructible</*is constructible*/ false, _IsReference, _Tp, _Args...> : public false_type {
};

  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值