Bootstrap

提升 C++ std::string 操作效率:善用 pop_back()

先说结论

咱直接先说结论吧,pop_back() 函数是用于删除 std::string 对象的最后一个字符,
实现很高效,本质上就是把要删除的最后一个字符置为null结束符,也就是置为: \0。

如果我们有这样的测试代码,那么 pop_back() 背后究竟做了哪些事情呢?

#include <string>
int main() {
	string strPhone = "150****1234";
	strPhone.pop_back();
	return 0;
}

看下源码

我的linux环境是gcc 4.8.5,c++的头文件位于/usr/include/c++
最终我们需要看的是这两个文件:
/usr/include/c++/4.8.5/bits/basic_string.h
/usr/include/c++/4.8.5/bits/basic_string.tcc

void pop_back() {
	erase(size()-1, 1);
}

GNU的c++实现,pop_back() 是调用了 erase() 函数来实现相关功能,正好可以顺道学习 erase() 函数了。
erase() 函数调用 _M_mutate() 函数来执行实际的删除操作,主要是更新字符串的内容和大小。
mutate是转变,转换的意思。

basic_string& erase(size_type __pos = 0, size_type __n = npos) {
	_M_mutate(
		_M_check(__pos, "basic_string::erase"),
		_M_limit(__pos, __n), 
		size_type(0)
	);
	return *this;
}

根据我们前述的测试代码,erase() 的第一个入参 __pos 就是 10,第二个入参 __n 就是 1 了。
代表 erase() 开始删除的第一个字符的索引是 10 (也就是最后一个字符 4 ),删除的字符数量是 1 个。

_M_mutate() 函数

_M_mutate() 函数略微复杂一点,我们等下再看。这里先放出函数的声明,其需要的是3个 size_type 类型的入参。
第一个参数 __pos 表示要开始修改的字符位置。
第二个参数 __len1 表示要删除的字符数量,由 _M_limit(__pos, __n) 计算得来。
第二个参数 __len2 表示要增加的字符数量。在我们讨论 pop_back() 这个场景下,是没有要增加的字符数量的,所以 erase() 调用 _M_mutate() 时,__len2 置为 0 。

void _M_mutate(size_type __pos, size_type __len1, size_type __len2)
{}

_M_check() 函数

然后我们来看传入 _M_mutate 的第一个参数: _M_check(__pos, “basic_string::erase”)
_M_check 是 std::string 的一个私有成员函数,就是检查一下给定的位置 __pos 是否在字符串的有效范围内。
如果 __pos (在本例中,是10)超出字符串的长度,则抛出 std::out_of_range 异常。
返回值仍然是 __pos。其实从函数本身来看是不需要返回值的,这里主要是方便操作。
就比如,其返回值可以作为 _M_mutate 的第一个入参。

size_type _M_check(size_type __pos, const char* __s) const {
	if (__pos > this->size())
		__throw_out_of_range(__N(__s));
	return __pos;
}

_M_limit() 函数

_M_limit 是 std::basic_string 类的一个私有成员函数,主要是用来限制偏移量以确保它不会超过字符串的有效范围。
用人话讲就是,现在要从 __pos 这个位置,再偏移 __off,看看是否会超出字符串的长度。

返回值代表限制之后的偏移量,确保它不会超过从 __pos 开始的字符串长度。
用人话讲就是,字符串的大小是11,现在要从位置 __pos (第 10 个字符)开始偏移,假如偏移量 __off 是 2,那就超过了字符串大小 11,我就限制一下,你只能偏移 1,不能偏移2。
这个返回值就是,经过计算之后的,偏移后不能超过字符串大小的,允许偏移的量。

在本例中,this->size() 是 11 ,第一个参数 __pos 就是 10,第二个入参 __off 就是 1。
所以 __testoff 计算得到是 false,代表没有超出范围,返回的就是原始的入参偏移量 __off。
如果 __testoff 计算得到是 tue,代表从位置 __pos 再偏移 __off 超出了范围,那就不能按入参 __off,而是可以偏移的量 this->size() - __pos。

size_type _M_limit(size_type __pos, size_type __off) const {
	const bool __testoff =  __off < this->size() - __pos;
	return __testoff ? __off : this->size() - __pos;
}

_M_mutate() 函数

最后我们来看一下 _M_mutate() 函数。
第二个参数 __len1 表示要删除的字符数量,我们这里就是 1。
第二个参数 __len2 表示要增加的字符数量,本例中是 0。

template<typename _CharT, typename _Traits, typename _Alloc>
void basic_string<_CharT, _Traits, _Alloc>::
_M_mutate(size_type __pos, size_type __len1, size_type __len2)
{
	const size_type __old_size = this->size(); // 计算之后 __old_size = 11
	const size_type __new_size = __old_size + __len2 - __len1; // 计算之后 __new_size = 10
	const size_type __how_much = __old_size - __pos - __len1; // 计算之后 __how_much = 0
	
	// 新大小超过当前容量,或者当前字符串是共享的(共享意味着多个字符串对象可能指向同一块内存)。
	// 就需要重新分配内存,然后执行拷贝动作
	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);
		
		// 拷贝 __pos 位置之前的数据
		if (__pos)
			_M_copy(__r->_M_refdata(), _M_data(), __pos);
		// __how_much 是指,计算从 __pos 开始,删除 __len1 个字符后剩余的字符数量
		// 这部分数据也需要拷贝到新字符串中
		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);
}

void _M_set_length_and_sharable(size_type __n)
{
 this->_M_set_sharable();  // One reference.
 this->_M_length = __n;
 // 将字符数组中索引为 __n 的位置赋值为 终止符 _S_terminal
 traits_type::assign(this->_M_refdata()[__n], _S_terminal);
}

_M_move() 函数

_M_move() 函数是 std::string 的一部分,并且是静态函数。主要用于在字符串中移动数据。
第一个入参 _CharT* __d :指向目标字符数组的指针,往哪儿去。
第二个入参 const _CharT* __s :指向源字符数组的指针,从哪儿来。
第三个入参 size_type __n :表示要移动的字符数量。
如果 __n 是 1,处理比较简单。如果移动多个字符,还需要处理内存重叠的问题。

static void _M_move(_CharT* __d, const _CharT* __s, size_type __n) {
	if (__n == 1) 
		traits_type::assign(*__d, *__s);
	else
		traits_type::move(__d, __s, __n);
}

关于 char_traits 模板类

上述的 _M_move() 函数中的 traits_type 只是 basic_string 模板类里面的一个类型定义。

template<typename _CharT, typename _Traits, typename _Alloc>
class basic_string
{
public:
	typedef _Traits	      traits_type;
};

我们可以来看一下这一组声明(我调整了顺序,方便理解)。
string 的 _Traits 类型 就是默认的 char_traits<_CharT> (下述代码第4行)

typedef basic_string<char>    string;  

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

char_traits 又是什么呢?它也是一个模板类。是用于定义和处理字符特性的基类。

template<typename _CharT>
struct char_traits
{
};

template<> struct char_traits<char>;
template<> struct char_traits<wchar_t>;
template<> struct char_traits<char16_t>;
template<> struct char_traits<char32_t>;

我们知道,basic_string 是一个模板类,字符类型不同,会有不同的 string。
只不过我们比较常用的是 string。

// /usr/include/c++/4.8.5/bits/stringfwd.h
typedef basic_string<char>    string;  
typedef basic_string<wchar_t> wstring;   
typedef basic_string<char16_t> u16string; 
typedef basic_string<char32_t> u32string; 

我们来看一下 string。
string 就是用来处理 char 这种字符的。
根据下面 basic_string 的声明,可以知道:
_CharT 类型就是 char 类型
_Traits 类型就是 char_traits 类型
_Alloc 类型就是 allocator 类型

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

template<typename _CharT, typename _Traits, typename _Alloc>
class basic_string
{
public:
	typedef _Traits	      traits_type;
};

所以,回到 _M_move() 函数中的那个 traits_type::assign() 和 traits_type::move()
其实就是 _Traits::assign() 和 _Traits::move()
也就是 char_traits::assign() 和 char_traits::move()
就是对 char 这种内置类型的 assign 和 move()。

在 /usr/include/c++/4.8.5/bits/char_traits.h 文件中,可以看到 struct char_traits 的声明。
当然 char_traits<wchar_t>、char_traits<char16_t>、char_traits<char32_t>这些类型,也都在这个文件里。

template<typename _CharT>
struct char_traits
{
	static void assign(char_type& __c1, const char_type& __c2)
	{ __c1 = __c2; }
	
	static char_type* move(char_type* __s1, const char_type* __s2, std::size_t __n);
};

char_traits 模板类只是提供了通用的操作,针对每一种特定的类型,又做了模板特化。

// 模板特化
template<>
struct char_traits<char>
{
	typedef char              char_type;
	  
	static void assign(char_type& __c1, const char_type& __c2)
	{ __c1 = __c2; }
	
	static char_type* move(char_type* __s1, const char_type* __s2, size_t __n)
	{ return static_cast<char_type*>(__builtin_memmove(__s1, __s2, __n)); }
};

// 模板特化
template<>
struct char_traits<wchar_t>
{
	typedef wchar_t           char_type;
	
	static void assign(char_type& __c1, const char_type& __c2)
	{ __c1 = __c2; }
	
	static char_type* move(char_type* __s1, const char_type* __s2, size_t __n)
	{ return static_cast<char_type*>(__builtin_memmove(__s1, __s2, __n)); }
};

// 模板特化
template<>
struct char_traits<char16_t>
{
	typedef char16_t          char_type;
	
	static void assign(char_type& __c1, const char_type& __c2) noexcept
	{ __c1 = __c2; }
	
	static char_type* move(char_type* __s1, const char_type* __s2, size_t __n)
	{
		return (static_cast<char_type*>
		(__builtin_memmove(__s1, __s2, __n * sizeof(char_type))));
	}
};

总结一下,basic_string 模板类如何通用地操作这些不一样的字符类型(char/wchar_t/char16_t/char32_t)呢?
标准库做了另外一个模板类:char_traits,用于提取出这些字符类型的通用操作特性。
然后再针对每一种字符类型,做模板特化。

;