具有自定义指针类型的内存分配器

Memory allocator with custom pointer type

本文关键字:内存 分配器 类型 指针 自定义      更新时间:2023-10-16

我试图创建一个使用智能指针的自定义内存分配器。我不发布代码,因为它太大并且没有添加太多信息。然后我用std::vector测试了它.它在Xcode上运行良好。但是当我尝试在Visual Studio 12(2013)中构建相同的代码时,构建失败并显示以下错误:

。vector(873): 错误 C2660: ' std::_Wrap_alloc< my_allocator< int > >::construct ' : 函数不接受 2 个参数

问题出在push_back方法上:

void push_back(value_type&& _Val)
    {
    ....
        this->_Getal().construct(this->_Mylast,
            _STD forward<value_type>(this->_Myfirst[_Idx]));
    ....
    }

错误消息有点令人困惑。真正的问题是this->_Mylastmy_allocator< int >::pointer类型,这是一个智能指针,构造方法期望int*

因此,问题很简单:自定义内存分配器中使用的指针类型有哪些要求?X::pointer应该转换为原始指针吗?如果是,这会使它们变得毫无用处。

实际上,我希望这行代码如下所示:

this->_Getal().construct(addressof(*(this->_Mylast)),
            _STD forward<value_type>(this->_Myfirst[_Idx]));

让我们尝试在C++标准中找到答案,它说:

[17.6.3.5-5] X型分配器必须满足可复制可构造(17.6.3.1)的要求。X::pointerX::const_pointerX::void_pointerX::const_void_pointer类型应满足 NullablePointer (17.6.3.3) 的要求。这些类型的构造函数、比较运算符、复制操作、移动操作或交换操作都不应通过异常退出。 X::pointerX::const_pointer还应满足随机访问迭代器的要求 (24.2)

如果我们看一下 NullablePointer 请求,它们增加了一些其他要求:

[17.6.3.3] NullablePointer 类型是支持 null 值的类似指针的类型。在以下情况下,P 类型满足 NullablePointer 的要求:
(1.1) — P 满足 EqualComparable、DefaultConstructible、CopyConstructible、CopyAssignable 和 Destructible 的要求。

如果我检查随机访问迭代器要求,我也没有发现任何明确提及其对原始指针的强制转换。但在少数地方使用addressof的方法(例如24.2.1-5)。

此外,它并不是 Microsoft std::vector 实现中唯一假定 X::pointer 和原始指针相等的地方。我想知道,我错过了什么?

编辑:我将在这里添加一段my_allocator定义:

class my_allocator
{
public:
typedef std::size_t          size_type;
typedef std::ptrdiff_t       difference_type;
typedef my_ptr<T>            pointer;
typedef my_ptr<const T>      const_pointer;
typedef T&                   reference;
typedef const T&             const_reference;
typedef T                    value_type;
typedef my_ptr<void>         void_pointer;
typedef my_ptr<const void>   const_void_pointer;
<constructors>
pointer allocate(size_type n, const_void_pointer = nullptr);
void deallocate(const pointer& ptr, size_type elements_num);
};

为了解决这个问题,我创建了一个to_raw_pointer函数,它恰好适用于任何实现operator->()的"花哨指针"。 你可以在libc++实现中找到它。

在这里:

template <class _Tp>
inline _LIBCPP_INLINE_VISIBILITY
_Tp*
__to_raw_pointer(_Tp* __p) _NOEXCEPT
{
    return __p;
}
template <class _Pointer>
inline _LIBCPP_INLINE_VISIBILITY
typename pointer_traits<_Pointer>::element_type*
__to_raw_pointer(_Pointer __p) _NOEXCEPT
{
    return _VSTD::__to_raw_pointer(__p.operator->());
}

它的工作原理是以非常规的方式调用operator->()。 这个运算符必须调用另一个operator->(),或者返回一个真正的指针。 实指针的重载会使用恒等函数破坏递归。 所以这将像这样使用:

this->_Getal().construct(__to_raw_pointer(this->_Mylast),
            _STD forward<value_type>(this->_Myfirst[_Idx]));

construct被指定为采用真正的指针,而不是花哨的指针。 并且没有指定从花哨指针到真实指针的隐式转换。 容器必须使用 to_raw_pointeraddressof 之类的东西。

容器还需要通过allocator_traits调用construct,而不是如图所示直接在存储的分配器上调用它。 这是为了允许constructallocator_traits"默认",而不是要求分配器实现construct

目前,operator*()operator->() 通常都要求花哨的指针在调用该运算符之前为非空。 但是,我预计将来operator->()将放宽这一要求。

更新

当我写上面的时候,我有点着急。 现在我有时间了,我将包括allocator::pointer类型的完整要求。 然而,在重新阅读这个问题时,我发现Maxym在这个问题中已经做得很好,所以我不会在这里重复它们。

在 std 中但不完全明显的一件事是四种指针类型之间的隐式和显式转换:pointerconst_pointervoid_pointerconst_void_pointer

implicit allocator pointer conversions:
+--------------------------------------+
| pointer      -->  const_pointer      |
|    |                   |            |
|    |      ---------     |            |
|   |/             _|  |/           |
| void_pointer -->  const_void_pointer |
+--------------------------------------+

explicit allocator pointer conversions:
+--------------------------------------+
| pointer           const_pointer      |
|   /|                  /|           |
|    |                    |            |
|    |                    |            |
| void_pointer      const_void_pointer |
+--------------------------------------+

也就是说,你可以从非const隐式转换为const,以及从非void转换为void,并且可以显式地从void转换为非void。 但是容器无法从allocator::const_pointerallocator::const_void_pointerconst_cast(丢弃const-ness)。 一旦容器const,就再也回不来了。