为什么不应该继承自std::allocator

21

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

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有什么问题吗?

我可以继承我的类吗?还是我需要一个虚拟析构函数来做到这一点?

为什么?


new (static_cast<T*> (ptr) )看起来像是一个笔误(函数已经使用了pointer == T*)。 - dyp
1
继承 std::vectorstd::string 是完全可以的。您可以通过私有或保护继承来安全地实现,并使用 using 关键字公开所需部分的接口。 - mip
4个回答

39
很多人会在这个帖子中发表自己的看法,认为不应该从std :: allocator 继承,因为它没有虚析构函数。他们会谈论多态性、切片和通过指向基类的指针进行删除等问题,但这些都不符合标准中17.6.3.5 [allocator.requirements]详细说明的分配器要求。除非有人证明从std :: allocator 派生的类未满足这些要求之一,否则这只是简单的盲目跟风。
话虽如此,在C++11中很少有理由从std :: allocator 派生。C++11对分配器进行了全面的改进,引入了traits模板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代码会因为未更新而报告缺少成员。


我喜欢这个!不知道我可以在没有显式创建分配器中的每个成员的情况下做到你那里的事情。谢谢你!我以为我必须以某种方式实现allocator_traits。我不知道容器会使用它。=)这正是我想知道的。 - Brandon
提供信息,我在clang上使用libc++和clang自己的c++abi尝试了std::list<int, my_allocator<int>>,并且编译成功。 - user534498
@user534498 libc++从未打算支持早于C++11的语言版本,因此我并不惊讶它在allocator_traits支持方面比libstdc++更进一步。感谢您的验证。 - Casey
实际上,我发现当从std::allocator继承时,GCC 5.3没有调用我的自定义deallocate(pointer, size_type)。这是派生类中提供的唯一成员函数。我还通过使用-O0进行测试验证了它不是优化器的问题。 - jww
1
潜在的从std::allocator派生的原因可能是需要对其行为进行仪器化。在这种情况下,人们可能希望记录一些数据,然后默认行为是allocatedeallocate。在这种情况下,公共继承似乎是一种简单的方法,并且std::iterator_trait仍然能够依赖于std::allocator的公共API的好处。 - Ad N
显示剩余2条评论

6
std::allocator<...>模板没有任何虚函数,因此,它显然不适合提供派生功能。尽管有些类或类模板即使没有虚析构函数和任何其他虚函数仍然是合理的基类,但这些往往是标签类型或使用奇异递归模板模式
分配器不打算像那样自定义,即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>());
}

1
我找不到任何使用std::allocator_traits的示例或其用途。我还注意到库正在使用类似于我的分配器并从中继承,但没有虚析构函数。我在某个地方读到过,分配器不应该有虚函数。http://gcc.gnu.org/ml/libstdc++/2011-05/msg00120.html - Brandon
1
你能演示一下在分配器要求(C++11 17.6.3.5)中详细说明的哪些操作有可能切掉逻辑吗? - Casey
@Casey:好的。明显的罪魁祸首是省略或拼错了 rebind。我已经更新了答案。 - Dietmar Kühl
如果表28的最后一列为给定表达式指定了默认值,则要求是可选的。在C++11中,重新绑定的正确方法是通过allocator_traits:typedef typename traits::template rebind_alloc<int> other;。或者直接重新绑定traits:typedef typename traits::template rebind_traits<int> otraits; typedef typename otraits::allocator_type other; - Casey
2
+1 对于打字错误是无声错误的来源 - 可能是最糟糕的错误类型 - 当公开继承时。这显然不是虚函数可以防止的问题:毕竟,成员模板没有“override”。 - Casey
显示剩余2条评论

0

我在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是Eigen自版本3.3.0以来的aligned_allocator


-1

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

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

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


6
在规范中详细说明分配器要求的地方并没有说分配器必须通过指向其基类之一的指针支持删除。您提出了一个很好的理由,不通常以多态方式处理没有虚析构函数的派生类,但这并没有回答原帖的问题,因为它不适用于分配器的特定情况。 - Casey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接