首页 » » C++

源码看stl版本演进中string实现的变化——拷贝/移动的构造和赋值的实质动作和效率

C++标准库STL中的字符串string是基本类型,标准进化过程中对string的最终接口和行为方式有过变化和调整,主要的是C++11要求string不能使用COW(Copy on Write)机制。C++11前很多STL实现(以HP STL为基础衍生)使用的是引用计数支持的写时拷贝方式,即所谓COW,C++11后的基本使用独立堆空间读写各自内存。

这个主要变化前后,string复制和构造的方式,执行过程中着重点,资源和效率都有不同,在源码实现分析下这些变化,可总结出使用string各程序环境的功能实施和效率影响。

代码分析以g++采用的SGI STL为例,gcc支持C++11在版本4.8.5,不同的实现在这个版本前后。

这两个实现主要方式:

C++11前的写时复制机制主要是通过引用计数的方法,把字符串内容相同的构造和赋值全部指向同一个实际数据空间,并通过记录引用数决定数据的创建和销毁,在共享同一个数据的几个string中有一个需要改变内容时,再拷贝一份数据由该string指向并修改内容。

C++11后独立空间的string一般由局部和堆两个空间配合,局部固定长度的空间存放短字符串,堆空间在数据超出局部空间时分配,多数一般的短字符串只使用局部空间读写,减少了大量动态堆内存分配/销毁。构造和修改时string作用自己的局部或堆空间。


1. C++11后独立空间string

C++11后string通过定义固定的小数组和堆指针,实现长短配合独立空间。多数的小字符串直接使用固定数组,实际空间对局部string在栈上,全局string在全局数据区,这样常规字符串和频繁使用的临时字符串不需要动态内存参与;字符串内容较多的长串出现时分配适当的堆内存(一般在内容的1倍到2倍之间),应对这种较少的情况。

这里有个标准升级后的相应问题,关于ABI(Application Binary Interface),应用程序二进制接口,即一种较低层的接口,是程序的二进制文件被别的二进制程序调用时约定的接口,涉及到函数调用、寄存器、栈形状等,可以理解为C++11对编译后程序的二进制调用方式,有新的标准定义或约定,这些方式包括程序调用库、程序调用其自身、库调用库的函数和数据的接口。C++11的string变化有些接口功能要求执行方式和二进制ABI相关并通过ABI表现出来,所以STL库C++11通过ABI版本判断同时条件编译决定了string的实现形式。SGI STL定义宏_GLIBCXX_USE_CXX11_ABI,并用这个宏标识string实现是C++11前还是后,打开这个宏,string是C++11后要求的独立空间方式。
这个定义的简要形式是:

#if _GLIBCXX_USE_CXX11_ABI
namespace std {
    class string {/*独立空间*/};
};
#else
namespace std {
    class string {/*写时拷贝*/};
}
#endif

C++11后独立空间string实现在条件编译定义_GLIBCXX_USE_CXX11_ABI=1时
实现分析代码是gcc 8.1.0版本,string的定义在4.8.5后基本上变化不大

string是模板类basic_string的一个特化,其中字符类型是char,char_traits是类型萃取器,实现一些字符相关性质判断,allocator是使用基本new和delete的空间分配器

template<typename _CharT, typename _Traits = char_traits<_CharT>,
           typename _Alloc = allocator<_CharT> >
class basic_string;

typedef basic_string<char>    string;

长短字符空间配合的方式实现基础是通过数据结构定义,这些是定义在basic_string中的private成员

    struct _Alloc_hider : allocator_type {
    #if __cplusplus < 201103L
        ...
    #else
        _Alloc_hider(pointer __dat, const _Alloc& __a)
        : allocator_type(__a), _M_p(__dat) { }

        _Alloc_hider(pointer __dat, _Alloc&& __a = _Alloc())
        : allocator_type(std::move(__a)), _M_p(__dat) { }
    #endif

        pointer _M_p; // The actual data.
    };

    _Alloc_hider    _M_dataplus;
    size_type       _M_string_length;

    enum { _S_local_capacity = 15 / sizeof(_CharT) };

    union
    {
        _CharT           _M_local_buf[_S_local_capacity + 1];
        size_type        _M_allocated_capacity;
    };

_Alloc_hider是实现了分配功能的实际数据的结构类型,_M_p是数据的指针,继承的allocator可以对数据分配/销毁。定义的_M_dataplus是_Alloc_hider类型,_M_string_length是字符串长度,不含结尾0
联合定义局部短字符空间_M_local_buf,这个变量同时可以在使用堆空间时存储空间容量_M_allocated_capacity,可以看到局部空间的固定长度16个字节,字符容量最长15个
_Alloc_hider的构造函数定义复制和移动分配器的方法,_M_p指向实际数据,使用局部空间时等于_M_local_buf地址,使用堆空间时由_Alloc_hider继承的分配器分配

独立空间的string构造函数定义了一组不同参数形式,其中主要的几个:

    basic_string()
        : _M_dataplus(_M_local_data())
        { _M_set_length(0); }

    basic_string(const basic_string& __str)
        : _M_dataplus(_M_local_data(),
              _Alloc_traits::_S_select_on_copy(__str._M_get_allocator()))
        { _M_construct(__str._M_data(), __str._M_data() + __str.length()); }

#if __cplusplus >= 201103L
    basic_string(basic_string&& __str) noexcept
        : _M_dataplus(_M_local_data(), std::move(__str._M_get_allocator()))
        {
            if (__str._M_is_local())
            {
                traits_type::copy(_M_local_buf, __str._M_local_buf,
                          _S_local_capacity + 1);
            }
            else
            {
                _M_data(__str._M_data());
                _M_capacity(__str._M_allocated_capacity);
            }

            // Must use _M_length() here not _M_set_length() because
            // basic_stringbuf relies on writing into unallocated capacity so
            // we mess up the contents if we put a '\0' in the string.
            _M_length(__str.length());
            __str._M_data(__str._M_local_data());
            __str._M_set_length(0);
        }
#endif // C++11

    basic_string(const _CharT* __s, const _Alloc& __a = _Alloc())
        : _M_dataplus(_M_local_data(), __a)
        { _M_construct(__s, __s ? __s + traits_type::length(__s) : __s+npos); }

    basic_string(size_type __n, _CharT __c, const _Alloc& __a = _Alloc())
        : _M_dataplus(_M_local_data(), __a)
        { _M_construct(__n, __c); }

#if __cplusplus >= 201103L
    template<typename _InputIterator, typename = std::_RequireInputIter<_InputIterator>>
#else
      ...
#endif
    basic_string(_InputIterator __beg, _InputIterator __end, const _Alloc& __a = _Alloc())
        : _M_dataplus(_M_local_data(), __a)
        { _M_construct(__beg, __end); }

构造几个主要类型,默认构造、复制构造、移动构造、C字符串构造、重复字符构造、迭代器构造,这些构造默认的数据指针初始指向局部空间,然后执行相应的构造动作,其中_M_local_data返回局部空间指针,_M_data返回实际数据指针,_M_construct执行构造动作,这几个函数实现:

    pointer
    _M_local_data()
    {
#if __cplusplus >= 201103L
        //指针类型traits建立指针函数,相当于取地址&
        return std::pointer_traits<pointer>::pointer_to(*_M_local_buf);
#else
        return pointer(_M_local_buf);  //转成指针类型,pointer是字符指针
#endif
    }

    pointer
    _M_data() const
    { return _M_dataplus._M_p; }

    bool
    _M_is_local() const
    { return _M_data() == _M_local_data(); }


    template<typename _InIterator>
    void
    _M_construct(_InIterator __beg, _InIterator __end)
    {
        size_type __dnew = static_cast<size_type>(std::distance(__beg, __end));

        if (__dnew > size_type(_S_local_capacity))
        {
            _M_data(_M_create(__dnew, size_type(0)));
            _M_capacity(__dnew);
        }

        // Check for out_of_range and length_error exceptions.
        __try
        { this->_S_copy_chars(_M_data(), __beg, __end); }
        __catch(...)
        {
            _M_dispose();
            __throw_exception_again;
        }

        _M_set_length(__dnew);
    }

    void
    _M_construct(size_type __n, _CharT __c)
    {
        if (__n > size_type(_S_local_capacity))
        {
            _M_data(_M_create(__n, size_type(0)));
            _M_capacity(__n);
        }

        if (__n)
            this->_S_assign(_M_data(), __n, __c); //重复复制n个字符

        _M_set_length(__n);
    }

    pointer
    _M_create(size_type& __capacity, size_type __old_capacity)
    {
        if (__capacity > max_size())
      std::__throw_length_error(__N("basic_string::_M_create"));

        // 每次空间不足时分配
        // 分配策略最少现有空间2倍,避免分配空间空闲太少频繁分配
        if (__capacity > __old_capacity && __capacity < 2 * __old_capacity)
        {
            __capacity = 2 * __old_capacity;
            if (__capacity > max_size())
                __capacity = max_size();
        }

        // 结尾0占用一个空间
        return _Alloc_traits::allocate(_M_get_allocator(), __capacity + 1);
    }

    void
    _M_capacity(size_type __capacity)
    { _M_allocated_capacity = __capacity; }

    template<class _Iterator>
    static void
    _S_copy_chars(_CharT* __p, _Iterator __k1, _Iterator __k2)
    {
        for (; __k1 != __k2; ++__k1, (void)++__p)
            traits_type::assign(*__p, *__k1); // 单个字符类型赋值
    }

    void
    _M_dispose() //弃置字符串,使用局部空间不需动作,使用堆空间销毁
    {
        if (!_M_is_local())
            _M_destroy(_M_allocated_capacity);
    }

    void
    _M_set_length(size_type __n) //设置字符串长度,并赋值结尾0
    {
        _M_length(__n);
        traits_type::assign(_M_data()[__n], _CharT());
    }

_M_construct首先按构造字符长度需要确定使用局部空间或调用_M_create分配堆空间,然后按参数执行逐个复制或重复复制。_M_create是对内存分配函数,构造和字符长度增长到空间不足时调用,重分配策略是至少现有空间2倍,需要在2倍和最大值之间按需要,避免扩容过小频繁分配

basic_string的几个构造函数的实际动作:

  • 默认构造 数据使用局部空间并清空字符串
  • 复制构造 数据初始指向局部空间,分配器复制构造同__str,执行构造动作,即按__str长度选择是否分配堆空间,复制__str内容
  • 移动构造 数据初始指向局部空间,分配器移动构造同__str,如果__str数据在局部,复制到this局部,__str数据在堆,直接复制指针,清空__str为局部空字符
  • C字符串构造 同复制构造过程,执行构造的迭代器换成C字符指针
  • 重复字符构造 同复制构造过程,执行构造动作重复复制
  • 迭代器构造 同复制构造过程

独立空间string赋值操作执行也是局部和堆空间配合,主要赋值操作符:

    basic_string&
    operator=(const basic_string& __str)
    {
#if __cplusplus >= 201103L
        if (_Alloc_traits::_S_propagate_on_copy_assign())
        {
            if (!_Alloc_traits::_S_always_equal() && !_M_is_local() 
                && _M_get_allocator() != __str._M_get_allocator())
            {
                // Propagating allocator cannot free existing storage so must
                // deallocate it before replacing current allocator.
                if (__str.size() <= _S_local_capacity)
                {
                    _M_destroy(_M_allocated_capacity);
                    _M_data(_M_local_data());
                    _M_set_length(0);
                }
                else
                {
                    const auto __len = __str.size();
                    auto __alloc = __str._M_get_allocator();
                    // If this allocation throws there are no effects:
                    auto __ptr = _Alloc_traits::allocate(__alloc, __len + 1);
                    _M_destroy(_M_allocated_capacity);
                    _M_data(__ptr);
                    _M_capacity(__len);
                    _M_set_length(__len);
                }
            }
            std::__alloc_on_copy(_M_get_allocator(), __str._M_get_allocator());
        }
#endif
        return this->assign(__str);
      }

#if __cplusplus >= 201103L
    basic_string&
    operator=(basic_string&& __str)
    {
        if (!_M_is_local() && _Alloc_traits::_S_propagate_on_move_assign()
            && !_Alloc_traits::_S_always_equal()
            && _M_get_allocator() != __str._M_get_allocator())
        {
            // Destroy existing storage before replacing allocator.
            _M_destroy(_M_allocated_capacity);
            _M_data(_M_local_data());
            _M_set_length(0);
        }
        // Replace allocator if POCMA is true.
        std::__alloc_on_move(_M_get_allocator(), __str._M_get_allocator());

        if (!__str._M_is_local() && (_Alloc_traits::_S_propagate_on_move_assign()
            || _Alloc_traits::_S_always_equal()))
        {
            pointer __data = nullptr;
            size_type __capacity;
            if (!_M_is_local())
            {
                if (_Alloc_traits::_S_always_equal())
                {
                    __data = _M_data();
                    __capacity = _M_allocated_capacity;
                }
                else
                    _M_destroy(_M_allocated_capacity);
            }

            _M_data(__str._M_data());
            _M_length(__str.length());
            _M_capacity(__str._M_allocated_capacity);
            if (__data)
            {
                __str._M_data(__data);
                __str._M_capacity(__capacity);
            }
            else
                __str._M_data(__str._M_local_buf);
        }
        else
            assign(__str);
        __str.clear();
        return *this;
    }

    basic_string&
    operator=(const _CharT* __s)
    { return this->assign(__s); }

可以看出,赋值操作和构造过程基本相似,只是少了初值指向局部,且对当前值和分配条件作了不同处理

  • 复制赋值 判断了当前值和分配器

    1. __str的分配器和当前分配器可互拷贝但不相同转2,否则直接复制__str内容,其中__str长度大于当前容量重分配堆内存;
    2. 如果当前使用堆内存,则销毁当前堆空间并数据指向局部空间,使用局部空间则不变;
    3. 如果__str长度小于局部空间长度,复制__str内容,__str大于局部空间,用__str的分配器分配堆内存,复制__str内容;
    复制过程总结即调整局部和堆空间符合待赋__str恰当状态,逐字符复制
  • 移动赋值 同样判断当前值和分配器,三种情况

    • 当前使用堆空间且分配器不同,销毁当前堆空间并指向局部空间,如果__str使用堆空间,复制__str堆指针并置0,__str使用局部空间,逐字符复制
    • 当前使用堆空间且分配器相同,如果__str使用堆空间,交换当前和__str的堆指针,__str使用局部空间,逐字符复制
    • 当前使用局部空间,直接复制__str内容,其中__str长度大于当前容量重分配堆内存

    最后清空待赋__str

  • C字符串赋值 直接复制,待赋长度大于当前容量重分配堆内存


总结

C++11后独立空间的string,执行和分配逻辑比较简单,string持有自己的内存空间,在短字符和长字符间作了策略处理,使移动构造部分情况也需逐字符复制,但是减少了常用临时的短字符动态内存分配,且复制短字符效率影响较小,故该策略作出了效率取舍,同时减少系统内存碎片,分配器和复制的底层操作也有优化。


2. C++11前写时复制string

C++11前由于STL标准没有规定,很多string实现版本用了一种效率和空间都有提升的策略,基于共享堆内存引用计数,修改时分配独立空间,即所谓写时复制COW。这种方式对字符串只分配一块堆内存,内存中存储引用计数和数据长度等变量,变量后空间放字符数据。构造和赋值string时,只对原字符串引用计数加1,新字符串指向同一堆内存。销毁string时,对指向的堆内存中引用计数减1,如果没有指向堆内存的引用销毁堆内存,否则有引用则保持堆内存。

引用计数的数据结构通过_Rep结构体实现

    struct _Rep_base
    {
        size_type        _M_length;
        size_type        _M_capacity;
        _Atomic_word        _M_refcount;
    };

    struct _Rep : _Rep_base
    {
        static const size_type    _S_max_size;
        static const _CharT    _S_terminal;

        static size_type _S_empty_rep_storage[];

        static _Rep&
        _S_empty_rep() _GLIBCXX_NOEXCEPT
        {
          //返回空字符串,静态成员_Rep_base数据全0,表示空字符
          void* __p = reinterpret_cast<void*>(&_S_empty_rep_storage);
          return *reinterpret_cast<_Rep*>(__p);
        }

        bool
        _M_is_leaked() const _GLIBCXX_NOEXCEPT
        { return this->_M_refcount < 0; }

        bool
        _M_is_shared() const _GLIBCXX_NOEXCEPT
        { return this->_M_refcount > 0; }

        void
        _M_set_leaked() _GLIBCXX_NOEXCEPT
        { this->_M_refcount = -1; }

        void
        _M_set_sharable() _GLIBCXX_NOEXCEPT
        { this->_M_refcount = 0; }

        void
        _M_set_length_and_sharable(size_type __n) _GLIBCXX_NOEXCEPT
        {
            //设置引用计数1次并设置长度和结尾0
            this->_M_set_sharable();  // One reference.
            this->_M_length = __n;
            traits_type::assign(this->_M_refdata()[__n], _S_terminal);
        }

        _CharT*
        _M_refdata() throw()
        { return reinterpret_cast<_CharT*>(this + 1); }

        _CharT*
        _M_grab(const _Alloc& __alloc1, const _Alloc& __alloc2)
        {
          return (!_M_is_leaked() && __alloc1 == __alloc2)
                  ? _M_refcopy() : _M_clone(__alloc1);
        }

        _CharT*
        _M_refcopy() throw()
        {
          __gnu_cxx::__atomic_add_dispatch(&this->_M_refcount, 1);
          return _M_refdata();
        }

        void
        _M_dispose(const _Alloc& __a) _GLIBCXX_NOEXCEPT
        {
          _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&this->_M_refcount);
          if (__gnu_cxx::__exchange_and_add_dispatch(&this->_M_refcount, -1) <= 0)
          {
              _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&this->_M_refcount);
              _M_destroy(__a);
          }
        }

        

_Rep_base中三个成员是引用计数使用的变量,_M_refcount是引用计数值,值代表3种状态,-1是非正常状态1次引用,0是正常状态1次引用,n>0是n+1次引用;_M_length是字符串长度;_M_capacity是数据空间容量。长度不算结尾0,字符串实际占用空间_M_length+1,空间实际容量_M_capacity+1。这三个变量全0表示空字符串,这后面有一个字节0表示空字符串数据,_Rep的静态成员_S_empty_rep_storage表示一个空字符串实例,即一个_Rep型数据全0结构体空间,转成_Rep型指针可用于空字符比较判断。

引用计数写时拷贝字符串内存布局.png

_S_max_size和_S_terminal是常数为字符最大长度和结尾0。
其中几个设置和判断状态的函数,_M_is_leaked()是否非正常状态,_M_set_leaked()设置非正常,_M_is_shared()是否2个及以上引用,_M_set_sharable()设置1个引用正常状态,_M_set_length_and_sharable(size_type __n)设置长度和1个引用正常状态。
引用计数的维护是_M_refcopy()和M_dispose(const _Alloc& __a)函数,分别表示引用拷贝即加1和引用销毁即减1,其中_M_refcount加减用了原子方法,基于_M_refcount声明为原子类型,计数减1后如果引用数小于等于-1即没有引用,销毁堆空间。
最后是引用数据判断和访问函数_M_refdata()和_M_grab(const _Alloc& __alloc1, const _Alloc& __alloc2),这需要结合后面空间分配结构查看

堆空间分配和销毁函数:

    static _Rep*
    _S_create(size_type __capacity, size_type __old_capacity, const _Alloc& __alloc)
    {
        if (__capacity > __old_capacity && __capacity < 2 * __old_capacity)
            __capacity = 2 * __old_capacity;
        size_type __size = (__capacity + 1) * sizeof(_CharT) + sizeof(_Rep);
        void* __place = _Raw_bytes_alloc(__alloc).allocate(__size);
        _Rep *__p = new (__place) _Rep;
        __p->_M_capacity = __capacity;
        __p->_M_set_sharable();
        return __p;
    }
    
    void
    _M_destroy(const _Alloc& __a) throw ()
    {
        const size_type __size = sizeof(_Rep_base) + (this->_M_capacity + 1) * sizeof(_CharT);
        _Raw_bytes_alloc(__a).deallocate(reinterpret_cast<char*>(this), __size);
    }

空间分配同样使用最小现有2倍的策略,这中间有部分调整页面相关策略针对具体malloc实现的多数操作系统设置最小值引起无效扩容,与此主题无关忽略了。可以看出,堆空间大小是数据空间+1结尾0+_Rep结构,再看_M_refdata()函数返回数据空间地址在一个_Rep结构之后,可确定堆空间内存布局是头部_Rep结构后面数据空间。_M_grab(const _Alloc& __alloc1, const _Alloc& __alloc2)是返回一个当前字符串的__alloc1分配器可用的拷贝,如果当前字符串可引用计数则返回引用拷贝(引用计数加1的当前字符串),不可引用则返回一个__alloc1分配空间并复制内容的独立拷贝。
_M_destroy(const _Alloc& __a)即正常用分配器销毁空间。

写时拷贝策略实现通过_M_replace_safe、_M_mutate几个函数,一个典型例子赋值函数assign:

    basic_string&
    assign(const _CharT* __s, size_type __n)
    {
      if (_M_disjunct(__s) || _M_rep()->_M_is_shared())
        return _M_replace_safe(size_type(0), this->size(), __s, __n);
      else
        {
          // Work in-place.
          const size_type __pos = __s - _M_data();
          if (__pos >= __n)
            _M_copy(_M_data(), __s, __n);
          else if (__pos)
            _M_move(_M_data(), __s, __n);
          _M_rep()->_M_set_length_and_sharable(__n);
          return *this;
        }
     }
     
    basic_string&
    _M_replace_safe(size_type __pos1, size_type __n1, const _CharT* __s, size_type __n2)
    {
      _M_mutate(__pos1, __n1, __n2);
      if (__n2)
        _M_copy(_M_data() + __pos1, __s, __n2);
      return *this;
    }
    
    void
    _M_mutate(size_type __pos, size_type __len1, size_type __len2)
    {
      const size_type __old_size = this->size();
      const size_type __new_size = __old_size + __len2 - __len1;
      const size_type __how_much = __old_size - __pos - __len1;

      if (__new_size > this->capacity() || _M_rep()->_M_is_shared())
        {
          // Must reallocate.
          const allocator_type __a = get_allocator();
          _Rep* __r = _Rep::_S_create(__new_size, this->capacity(), __a);

          if (__pos)
            _M_copy(__r->_M_refdata(), _M_data(), __pos);
          if (__how_much)
            _M_copy(__r->_M_refdata() + __pos + __len2,
                    _M_data() + __pos + __len1, __how_much);

          _M_rep()->_M_dispose(__a);
          _M_data(__r->_M_refdata());
        }
      else if (__how_much && __len1 != __len2)
        {
          // Work in-place.
          _M_move(_M_data() + __pos + __len2,
                  _M_data() + __pos + __len1, __how_much);
        }
      _M_rep()->_M_set_length_and_sharable(__new_size);
    }

其中assign的写时拷贝发生在if分支当前字符串和__s空间不重叠_M_disjunct或有共享引用_M_is_shared,执行_M_replace_safe。执行空间调整_M_mutate,分两种情况,如果没有共享引用,需要赋值的字符串超过当前容量则重新分配堆空间,不超过直接返回执行赋值;如果有共享引用,新分配独立堆空间并复制源内容,这里即实现了写时拷贝策略,返回执行赋值。赋值是逐字符复制_M_copy。

写时拷贝策略string的构造函数,主要的几个:

   basic_string()
   : _M_dataplus(_S_construct(size_type(), _CharT(), _Alloc()), _Alloc()){ }
   
    basic_string(const basic_string& __str)
    : _M_dataplus(__str._M_rep()->_M_grab(_Alloc(__str.get_allocator()), __str.get_allocator()),
                  __str.get_allocator()){ }
    
    basic_string(basic_string&& __str)
    : _M_dataplus(__str._M_dataplus)
      { __str._M_data(_S_construct(size_type(), _CharT(), get_allocator())); }

    static _CharT*
    _S_construct(size_type __n, _CharT __c, const _Alloc& __a)
    {
      // Check for out_of_range and length_error exceptions.
      _Rep* __r = _Rep::_S_create(__n, size_type(0), __a);
      if (__n)
        _M_assign(__r->_M_refdata(), __n, __c);

      __r->_M_set_length_and_sharable(__n);
      return __r->_M_refdata();
    }

构造分类中典型的是默认构造、复制构造、移动构造,主要的是涉及应用引用计数,复制时状态判断,重复字符分配空间问题,构造时的实际动作:

  • 默认构造 直接分配了无空间空内容引用1的数据
  • 复制构造 对待复制_str分情况处理,如果_str可引用计数则计数加1,不可计数分配新空间复制_str
  • 移动构造 对_str包括引用计数和长度的整个堆空间进行移动,并再分配空字符1引用给_str
    _S_construct函数分配引用计数堆空间,长度_n,引用数1,并重复_c字符_n次赋值

写时拷贝的string构造的特点是复制只是引用计数增加,不复制

赋值操作符定义主要的几个:

    basic_string&
    operator=(const basic_string& __str) 
    { return this->assign(__str); }
    
    basic_string&
    operator=(const _CharT* __s) 
    { return this->assign(__s); }
    
    basic_string&
    operator=(basic_string&& __str)
    {
        this->swap(__str);
        return *this;
    }

赋值操作符执行的是写时拷贝的策略,调用的assign函数前面分析了,移动赋值同样针对整个引用计数的堆空间,执行的实际动作:

  • 复制赋值 执行assign函数,实施写时拷贝策略
  • C字符串赋值 同样执行assign
  • 移动赋值 对整个引用计数堆空间,如果_str和当前字符串分配器相同,直接交换堆空间,不相同则分别用对应分配器分配新空间,交换赋值内容


总结

C++11前的写时拷贝策略的string特点是一整个堆内存存储引用计数、长度、容量等变量和数据内容,对同内容的字符构造共享已有堆内存仅增加引用计数,一定程度节省内存空间。但是构造、赋值和销毁时对引用计数处理执行的运行判断步骤也有一定成本,只是小于堆内存分配,还面临一个问题是共享的引用计数堆空间在多线程中需要数据同步,即使实际写时拷贝的都是只有一个引用的独立空间


3. 实质动作和效率影响对使用string的考量

C++11标准的修改对string的实现产生了基础策略层面的变化,导致行为影响作用不同,使用时需要对各种不同情况分析实质动作,对使用方式的正确和效率准确判定。
C++11后独立空间的string数据空间字符串自己持有,但设计了长短空间区分局部和堆内存,这时局部变量和类成员变量的字符串频繁使用临时构造销毁时,减少了堆内存分配次数,同时全部局部独立复制不需数据同步,正确可以预判,且效率较高。但是这种字符串需要传递出作用域时,不论是构造新字符串或赋值给其他字符串都会逐字符复制,因此应尽可能使用移动构造或赋值。
C++11前引用计数的写时拷贝string,充分利用相同字符和内容不变的状态,使用类似智能指针的共享引用计数策略,在需要改变的最后一刻复制字符串,这样的策略节省了空间,不论临时局部字符串或传递进出作用域,传递时使用复制或移动方式,在内容不变时都没有效率的额外较大开销,缺点也很明显,内容变动充分的情况会持续堆内存分配,不论内容长短。

使用是典型场景示例分析:

void temp_string(string origin){
    //temp是局部串
    //C++11后如果是短字符使用局部空间提倡这样写,如果长字符直接用origin
    //C++11前引用计数如果后面using中不改变这样写,如果改变建议直接append确定分配空间
    string temp = origin;  
    //another同temp,只是构造换为赋值
    string another;
    another = origin;
    //using temp and another
    ...
    //end using
    if(origin == temp){  //局部串msg调用移动构造,这样写或直接用origin + " equal"差别不大
        string msg = origin + " equal";
        cout<<msg<<endl;
    } else {
        string msg = origin + " not equal";
        cout<<msg<<endl;
    }
}

string global;

void using_string(string arg){  //值传递,C++11前引用计数使用
void using_string(const string &arg){  //左值引用,C++11前同值传递差别不大,C++11后使用不需复制
void using_string(string &&arg){  //右值引用,C++11前后都是移动,只是C++11前不销毁只是引用计数减1
    cout<<arg<<endl;
}

void pass_string(){
    string temp;
    temp = "temp to global";
    global = temp;  //C++11前引用计数传递
    global = move(temp); //C++11后移动赋值
    using_string(temp); //函数定义原则见实现
    
    vector<string> vec;
    string obj;
    vec.push_back(obj); //C++11前引用计数构造
    vec.push_back(move(obj)); //C++11后移动构造
    vec.emplace_back(obj); //C++11后转发传递,复制构造
    vec.emplace_back(move(obj)); //C++11后转发传递,移动构造
}

string return_string(){
    string local = "local return";
    //这里不涉及string实现,关系编译优化RVO原则
    //返回局部变量,如果编译器优化打开且支持,优化等级不同可能以下三种行为
    //在调用函数时返回赋值的变量直接构造"local return"
    //构造一次local,再构造一次返回赋值的变量
    //构造一次local,构造一个临时返回变量,再构造一次返回赋值的变量
    return local;
    //如果编译器不支持RVO或不打开优化,直接使用move后返回局部变量,出现以下两种行为的一种
    //构造一次local,再移动给返回赋值的变量
    //构造一次local,移动给一个临时返回变量,再移动给返回赋值的变量
    return move(local);
}

int main(){
    string origin = "origin";
    
    temp_string(origin);  //局部字符串行为和使用建议见定义
    pass_string();  //字符串传递出作用域行为和使用建议见定义
    
    string ret_rvo = return_string();  //返回值RVO优化见定义
}

文档信息

  • 本文标签: STL, 复制移动
  • 授权条款:GNU Verbatim 全文逐字复制许可证
    Verbatim copying and distribution of this entire article are permitted worldwide, without royalty, in any medium, provided this notice, and the copyright notice, are preserved.
    Read more about this license at http://www.gnu.org/
  • 发表日期:2024年06月26日,更新日期:2024年07月08日
  • 更多内容:档案 » C++
  • 相关文章

    • 无相关文章

    发表看法