深入解析std::allocator

std::allocator类模板定义在头文件中,是C++标准库容器默认的内存分配器实现,它封装了包括访问与寻址,分配与释放和对象构建与析构的策略。std::allocator是无状态的,也就是说,所有的allocator实例都是可互换的,可比较且相等的,甚至可以用一个allocator实例释放另一个allocator实例分配的内存。

/* 代码示例 */
std::allocator<int> a1;
std::allocator<int> a2;

int *p = a1.allocate(1); // 分配一个int所需要的空间
a2.deallocate(p, 1); // 释放由a1分配的空间

std::allocator类模板的典型用法如下,

/* 代码示例 */
std::allocator<std::string> a;
std::string* s = a.allocate(1); // 分配一个std::string所需要的空间
a.construct(s, "foo"); // 在之前分配的空间中构造一个std::string,其初始值为foo
a.destroy(s); // 析构之前构造的std::string,但空间并未释放
a.deallocate(s, 1); // 释放最初分配的空间

源码分析

以下源码分析基于gcc 9.2.0和LLVM clang 9.0.1。

类模板定义

/* gcc */
template<typename _Tp>
class allocator : public __allocator_base<_Tp>template<typename _Tp>
using __allocator_base = __gnu_cxx::new_allocator<_Tp>;

template<typename _Tp>
class new_allocator;
/* clang */
template <class _Tp>
class allocator;

可以看出,clang的定义要更简单一些。
gcc的std::allocator是继承自__gnu_cxx::new_allocator类模板的,后者是一个精确匹配C++标准allocator的实现,也就是分配内存使用全局new操作符,释放内存使用全局delete操作符。gcc的allocator成员函数实现都在__gnu_cxx::new_allocator中。

成员类型定义

std::allocator定义了一些成员类型,其中有一个特殊的rebind类型,它用来将一个std::allocator类型转换成另外一个std::allocator类型。

std::allocator<int>                      a1;
decltype(a1)::rebind<char>::other        a2; // 相当于std::allocator a2
std::allocator<int>::rebind<char>::other a3; // 相当于std::allocator a3

rebind类型的实现也非常简单,只包含如下的类型定义。

template <class _Up> struct rebind {typedef allocator<_Up> other;};

值得注意的是,rebind类型在C++17中被废弃,将会在C++20中被移除。

成员变量

std::allocator的实现没有成员变量,这也说明了它是无状态的。

构造/析构函数

std::allocator定义的多个构造函数,包括拷贝构造函数,以及析构函数,它们的实现都是空函数,不做任何操作。

成员函数

address成员函数

pointer std::allocator<T>::address( reference x ) const;

用于获取引用x的内存地址,即使在地址运算符&被重载的情况下也可以正常工作。gcc和clang的实现基本相同,代码如下:

pointer address(reference __x) const {
    return std::__addressof(__x);
}

template<typename _Tp>
inline _Tp* __addressof(_Tp& __r) {
     return __builtin_addressof(__r);
}

__builtin_addressof是一个内置函数,用来执行内置地址操作符&的取地址功能,忽略任何地址操作符&重载。clang实现中,存在__builtin_addressof内置函数时使用此内置函数获取地址,不存在此内置函数时,使用如下代码达到同样目的。

template <class _Tp>
inline _Tp* addressof(_Tp& __x) {
    return reinterpret_cast<_Tp *>(const_cast<char *>(&reinterpret_cast<const volatile char &>(__x)));
}

注意此函数在C++17中被废弃,将会在C++20中被移除。

max_size成员函数

size_type std::allocator<T>::max_size() const;

此函数返回最大的可分配数量,它的实现很简单,

size_type max_size() const {
    return size_type(~0) / sizeof(_Tp);
}

注意此函数在C++17中被废弃,将会在C++20中被移除。

allocate成员函数

pointer std::allocator<T>::allocate( size_type n, const void * hint = 0 );

用来分配n*sizeof(T)字节的未初始化内存空间,其中hint参数的本意是使allocate分配尽量靠近hint指针的内存,但是在实现中并没有使用,并且在C++17中增加了一个没有hint参数的allocate成员函数。
gcc和clang的实现大同小异,都是使用全局new操作符分配n*sizeof(T)字节的内存空间。不同的是,当检测到n过大时(n>this->max_size()),gcc抛出bad_alloc异常,而clang抛出length_error异常。

/* gcc */
pointer allocate(size_type __n, const void* = static_cast<const void*>(0)) {
    if (__n > this->max_size())
	    std::__throw_bad_alloc();

    return static_cast<_Tp*>(::operator new(__n * sizeof(_Tp)));
}
/* clang */
pointer allocate(size_type __n, allocator<void>::const_pointer = 0) {
    if (__n > max_size())
        __throw_length_error("allocator::allocate(size_t n)"
                             " 'n' exceeds maximum supported size");
    return static_cast<pointer>(::operator new(__n * sizeof(_Tp)));
}

deallocate成员函数

void deallocate(pointer __p, size_type __n);

用来释放__p所指向的allocate分配的内存空间,参数__n指之前分配的数量,此参数在gcc中并没有使用,在clang中根据编译条件的不同来决定使用与否。

/* gcc */
void deallocate(pointer __p, size_type) {
    ::operator delete(__p);
}
/* clang */
void deallocate(pointer __p, size_type __n) {
#ifdef _LIBCPP_HAS_NO_SIZED_DEALLOCATION
    return ::operator delete(__ptr);
#else
    return ::operator delete(__ptr, __n*sizeof(T)); // 具有内存大小参数的delete操作符
#endif
}

construct成员函数

template<typename _Up, typename... _Args>
void construct(_Up* __p, _Args&&... __args);

用来在__p所指向的内存中构建一个_Up类型的对象,并将__args参数列表传给_Up类型的构造函数。gcc和clang的实现相同,都是使用放置式new在制定内存中构建对象,并使用std::forward转发模板参数到构造函数。

template<typename _Up, typename... _Args>
void construct(_Up* __p, _Args&&... __args) {
    ::new((void *)__p) _Up(std::forward<_Args>(__args)...);
}

注意此函数在C++17中被废弃,将会在C++20中被移除。

destroy成员函数

template<typename _Up>
void destroy(_Up* __p);

用于析构__p所指向对象但并不释放内存。
此函数gcc和clang实现相同,都是直接调用析构函数。

template<typename _Up>
void destroy(_Up* __p) {
    __p->~_Up();
}

注意此函数在C++17中被废弃,将会在C++20中被移除。

模板特化

除了通用的allocator模板,gcc和clang都实现了某些类型的模板特化,包括allocatorallocator等。
其中allocator偏特化模板在gcc和clang中实现完全不同,

/* gcc */
template<typename _Tp>
class allocator<const _Tp> {
public:
    typedef _Tp value_type;
    template<typename _Up> allocator(const allocator<_Up>&) { }
};

gcc中的allocator偏特化完全是一个无效的allocator类型,分配释放等成员函数都没有定义。而clang中的allocator偏特化模板和通用allocator<_Tp>模板一样,拥有全部的成员函数。

你可能感兴趣的:(C++,c++)