为啥不从 std::allocator 继承

Posted

技术标签:

【中文标题】为啥不从 std::allocator 继承【英文标题】:Why not to inherit from std::allocator为什么不从 std::allocator 继承 【发布时间】:2014-01-31 15:19:27 【问题描述】:

我像这样创建了自己的分配器:

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&lt;T*&gt; (ptr) ) 看起来像是一个错字(该函数已经使用了pointer == T* std::vectorstd::string之后继承完全没问题。您可以使用私有或受保护的继承安全地执行此操作,然后使用 using 关键字公开其接口的所需部分。 【参考方案1】:

很多人会在这个帖子中发布你不应该从std::allocator 继承的帖子,因为它没有虚拟析构函数。他们将通过指向基类的指针讨论多态性和切片和删除,这些都不是标准第 17.6.3.5 节 [allocator.requirements] 中详述的分配器要求所允许的。在有人证明派生自 std::allocator 的类无法满足其中一个要求之前,这就是简单的货物崇拜心态。

也就是说,几乎没有理由从 C++11 中的std::allocator 派生。 C++11 对分配器的大修引入了特征模板std::allocator_traits 位于分配器及其用户之间,并通过模板元编程为许多所需功能提供合理的默认值。 A minimal allocator in C++11 can be as simple as:

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 。我不知道容器使用了它。 =) 这正是我想知道的。 为了您的信息,使用 libc++ 和 clang 自己的 c++abi 在 clang 上尝试了 std::list>,并且编译良好。 @user534498 libc++ 从未打算支持早于 C++11 的语言版本,所以我并不惊讶它在 allocator_traits 支持方面比 libstdc++ 更进一步。感谢您的验证。 在实践中我发现 GCC 5.3 在继承 std::allocator 时没有调用我的自定义 deallocate(pointer, size_type)。它是派生类中提供的唯一成员函数。我还通过-O0 测试验证了它不是优化器。 嗯,从std::allocator 派生的一个潜在原因是需要检测它的行为。在这种情况下,可能想要记录一些数据,然后是allocatedeallocate 的默认行为。在这种情况下,公共继承似乎是一种简单的方法,其好处是 std::iterator_trait 仍然能够依赖来自 std::allocator 的公共 API。【参考方案2】:

类模板std::allocator&lt;...&gt; 没有任何虚函数。因此,提供派生功能显然是一个糟糕的选择。尽管某些类或类模板仍然是合理的基类,即使没有虚拟析构函数和任何其他虚拟函数,它们往往只是标记类型或使用Curiously recurring template pattern。

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

std::allocator&lt;T&gt; 派生的主要问题是它可能隐藏rebind 成员的问题,例如,成员被省略或拼写错误。下面是一个应该打印my_allocator::allocate() 两次但不是由于拼写错误的示例。我认为my_allocator&lt;T&gt; 是一个完整的分配器,即使没有来自std::allocator&lt;T&gt; 的继承,即不必要的继承只会导致隐藏错误的可能性。您也可能会收到错误消息,例如,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>());

【讨论】:

我找不到任何使用 std::allocator_traits 的示例或其用途。我还注意到库使用类似于我的分配器并从它继承而没有虚拟析构函数。我在某处读到分配器不应该有虚函数。 gcc.gnu.org/ml/libstdc++/2011-05/msg00120.html 你能证明分配器要求(C++11 17.6.3.5)中详述的哪些操作有可能切断逻辑吗? @Casey:当然。明显的罪魁祸首是省略或拼写错误rebind。我已经更新了答案。 “如果表 28 的最后一列指定给定表达式的默认值,则要求是可选的。”在 C++11 中重新绑定的正确方法是通过 allocator_traits:typedef typename traits::template rebind_alloc&lt;int&gt; other;。或者直接重新绑定特征:typedef typename traits::template rebind_traits&lt;int&gt; otraits; typedef typename otraits::allocator_type other; +1 表示拼写错误是无声错误的来源——可能是更严重的错误——当公开继承时。这显然不是虚函数可以防止的问题:毕竟成员模板没有override【参考方案3】:

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

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

如果myallocator继承std::allocator,它必须重写任何可能与被重写时的类型不同的返回类型的父方法。

请注意,据我所知,这仅出现在 VS2013 中,因此您可能会认为这是编译器的问题,而不是代码的问题。

我在 Eigen 中使用的 myallocatoraligned_allocator,从 3.3.0 版本开始。

【讨论】:

【参考方案4】:

嗯,析构函数is not virtual。如果您不以多态方式使用分配器,这不是直接问题。但是考虑这种情况,BasicAllocator 继承自 std::allocator

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

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

【讨论】:

在标准对分配器要求的详细说明中没有说分配器必须通过指向其基类之一的指针来支持删除。您提出了一个很好的理由,一般来说,不要以多态方式处理从另一个没有虚拟析构函数的类派生的类,但它不能回答 OP 的问题,因为它不适用于分配器的特定情况。

以上是关于为啥不从 std::allocator 继承的主要内容,如果未能解决你的问题,请参考以下文章

为啥我们不能在不从 NSObject 继承类的情况下迅速采用协议?

为啥 `IList<T>` 不从 `IReadOnlyList<T>` 继承?

std :: list对其allocator参数做了什么?

如何强制库使用自定义 std::allocator?

利用std::allocator实现自定义的vector类

g++ 编译错误:std::ctype<char>、std::__detail、std::allocator<Taquart