为什么不从std::allocater继承呢

Why not to inherit from std::allocator

本文关键字:继承 allocater 为什么不 std      更新时间:2023-10-16

我创建了自己的分配器,如下所示:

template<typename T>
class BasicAllocator
{
public:
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
typedef T value_type;

BasicAllocator() throw() {};
BasicAllocator (const BasicAllocator& other) throw() {};
template<typename U>
BasicAllocator (const BasicAllocator<U>& other) throw() {};
template<typename U>
BasicAllocator& operator = (const BasicAllocator<U>& other) {return *this;}
BasicAllocator<T>& operator = (const BasicAllocator& other) {return *this;}
~BasicAllocator() {}
pointer address (reference value) const {return &value;}
const_pointer address (const_reference value) const {return &value;}
pointer allocate (size_type n, const void* hint = 0) {return static_cast<pointer> (::operator new (n * sizeof (value_type) ) );}
void deallocate (void* ptr, size_type n) {::operator delete (static_cast<T*> (ptr) );}
template<typename U, typename... Args>
void construct (U* ptr, Args&&  ... args) {::new (static_cast<void*> (ptr) ) U (std::forward<Args> (args)...);}
void construct (pointer ptr, const T& val) {new (static_cast<T*> (ptr) ) T (val);}
template<typename U>
void destroy (U* ptr) {ptr->~U();}
void destroy (pointer ptr) {ptr->~T();}
size_type max_size() const {return std::numeric_limits<std::size_t>::max() / sizeof (T);} /**return std::size_t(-1);**/
template<typename U>
struct rebind
{
typedef BasicAllocator<U> other;
};
};

但我想知道为什么我永远不应该继承std::allocator。是因为它没有虚拟析构函数吗?我看到很多帖子说,一个人应该创建自己的,而不是继承。我理解为什么我们不应该继承std::stringstd::vector,但继承std::allocator有什么错?

我可以继承上面的类吗?或者我需要一个虚拟析构函数来做这件事?

为什么?

很多人会在这个线程中发布不应该从std::allocator继承的内容,因为它没有虚拟析构函数。他们将讨论多态性以及通过指向基类的指针进行切片和删除,如标准第17.6.3.5[allocater.requirements]节所述,分配器需求不允许这些操作。除非有人证明从std::allocator派生的类不能满足其中一个要求,否则这只是一种简单的货物崇拜心态。

也就是说,没有什么理由从C++11中的std::allocator派生。C++11对分配器的彻底改革引入了特性模板std::allocator_traits,它位于分配器及其用户之间,并通过模板元编程为许多所需的特性提供合理的默认值。C++11中的最小分配器可以简单到:

template <typename T>
struct mallocator {
using value_type = T;
mallocator() = default;
template <class U>
mallocator(const mallocator<U>&) {}
T* allocate(std::size_t n) {
std::cout << "allocate(" << n << ") = ";
if (n <= std::numeric_limits<std::size_t>::max() / sizeof(T)) {
if (auto ptr = std::malloc(n * sizeof(T))) {
return static_cast<T*>(ptr);
}
}
throw std::bad_alloc();
}
void deallocate(T* ptr, std::size_t n) {
std::free(ptr);
}
};
template <typename T, typename U>
inline bool operator == (const mallocator<T>&, const mallocator<U>&) {
return true;
}
template <typename T, typename U>
inline bool operator != (const mallocator<T>& a, const mallocator<U>& b) {
return !(a == b);
}

编辑:std::allocator_traits的正确使用还没有完全出现在所有的标准库中。例如,上面的示例分配器在使用GCC 4.8.1编译时无法正确使用std::list——std::list代码抱怨缺少成员,因为它还没有更新。

类模板std::allocator<...>没有任何虚拟函数。因此,提供派生功能显然是一个糟糕的候选者。虽然有些类或类模板仍然是合理的基类,即使没有虚拟析构函数和任何其他虚拟函数,但它们往往只是标记类型,或者使用Curioly递归模板模式。

分配器不打算像那样进行自定义,即std::allocator<T>不打算作为基类。如果你试图这样使用它,你的逻辑可能很容易被切断。用于轻松定制分配器的方法是依靠std::allocator_traits<A>来提供你的分配器选择不显式提供的各种操作,使用基于相对少量操作的默认实现。

std::allocator<T>派生的主要问题是,它可能隐藏rebind成员的问题,例如,该成员被省略或拼写错误。下面是一个应该打印my_allocator::allocate()两次但由于打字错误而没有打印的示例。我认为my_allocator<T>是一个完整的分配器,即使没有来自std::allocator<T>的继承,也就是说,不必要的继承只会导致隐藏错误的可能性。您也可能会出现错误,例如allocate()deallocate()函数出错。

#include <memory>
#include <iostream>
template <typename T>
struct my_allocator
: std::allocator<T>
{
my_allocator() {}
template <typename U> my_allocator(my_allocator<U> const&) {}
typedef T value_type;
template <typename U> struct rebimd { typedef my_allocator<U> other; };
T* allocate(size_t n) {
std::cout << "my_allocator::allocate()n";
return static_cast<T*>(operator new(n*sizeof(T)));
}
void deallocate(T* p, size_t) { operator delete(p); }
};
template <typename A>
void f(A a)
{
typedef std::allocator_traits<A>    traits;
typedef typename traits::value_type value_type;
typedef typename traits::pointer    pointer;
pointer p = traits::allocate(a, sizeof(value_type));
traits::deallocate(a, p, sizeof(value_type));
typedef typename traits::template rebind_alloc<int> other;
typedef std::allocator_traits<other> otraits;
typedef typename otraits::value_type ovalue_type;
typedef typename otraits::pointer    opointer;
other o(a);
opointer op = otraits::allocate(o, sizeof(ovalue_type));
otraits::deallocate(o, op, sizeof(ovalue_type));
}
int main()
{
f(my_allocator<int>());
}

我刚刚在VS2013中遇到了一个关于此的问题(但在VS2015中没有出现)。可能不是这个问题的答案,但我无论如何都会分享这个:

在boost中,有一个函数call_select_on_container_copy_construction()测试分配器是否有成员select_on_container_copy_construction(),并调用该函数以获得分配器的副本。当std::allocator返回其自身的副本时,派生的myallocator应该重写此方法以执行相同操作并返回myallocator类型,而不是让它继承具有std::allocator返回类型的方法。这会导致类型不匹配的编译错误。

如果myallocator继承了std::allocator,则它必须重写任何父方法,这些父方法可能与重写时的类型不具有相同的返回类型。

注意,据我所见,这只出现在VS2013中,所以你可能会认为这是编译器的问题,而不是代码的问题。

我使用的myallocator是自版本3.3.0以来Eigen中的aligned_allocator

好吧,析构函数不是virtual。如果您不以多态方式使用分配器,这就不是一个直接的问题。但考虑一下这种情况,其中BasicAllocator继承自std::allocator:

std::allocator<int>* ptr = new BasicAllocator<int>();
// ...
delete ptr;

BasicAllocator的析构函数从未被调用,从而导致内存泄漏。