Почему бы не наследовать от std:: allocator
Я создал свой собственный распределитель следующим образом:
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::string
и std::vector
, но что не так с наследованием std::allocator
?
Можно ли наследовать мой класс выше? Или мне нужен виртуальный деструктор для этого?
Почему?
Ответы
Ответ 1
Многие люди собираются публиковать в этом потоке, что вы не должны наследовать от std::allocator
, потому что у него нет виртуального деструктора. Они будут говорить о полиморфизме и разрезании и удалении с помощью класса указатель-базовый, ни один из которых не разрешен требованиями распределителя, как подробно описано в разделе 17.6.3.5 [allocator.requirements] стандарта. Пока кто-то не продемонстрирует, что класс, полученный из std::allocator
, не отвечает одному из этих требований, это простой менталитет культа.
Тем не менее, нет оснований извлекать из std::allocator
в С++ 11. С++ 11 капитальный ремонт распределителей представил шаблон признаков std::allocator_traits
, чтобы сидеть между распределителем и его пользователями и предоставлять разумные значения по умолчанию для многих необходимых функций с помощью метапрограммирования шаблонов. Минимальный распределитель в С++ 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);
}
EDIT: правильное использование std::allocator_traits
пока не полностью присутствует во всех стандартных библиотеках. Например, вышеописанный распределитель выборки не работает с std::list
при компиляции с GCC 4.8.1 - код std::list
жалуется на отсутствие элементов, поскольку он еще не обновлен.
Ответ 2
Шаблон класса 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>());
}
Ответ 3
Ну, деструктор не virtual
. Это не является прямой проблемой, если вы не используете распределитель полиморфно. Но рассмотрим этот случай, где BasicAllocator
наследует от std::allocator
:
std::allocator<int>* ptr = new BasicAllocator<int>();
// ...
delete ptr;
Деструктор BasicAllocator
никогда не вызывается, что приводит к утечке памяти.
Ответ 4
Я просто попал в проблему в 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
Я использовал aligned_allocator
в Eigen с версии 3.3.0.