Должен ли я удалить конструктор перемещения и назначение перемещения интеллектуального указателя?
Я реализую простой умный указатель, который в основном отслеживает количество ссылок на указатель, который он обрабатывает.
Я знаю, что могу реализовать семантику перемещения, но я не думаю, что это имеет смысл, поскольку копирование умного указателя очень дешево. Особенно с учетом того, что он дает возможность создавать неприятные ошибки.
Вот мой код C++11 (я пропустил некоторый несущественный код). Общие комментарии также приветствуются.
#ifndef SMART_PTR_H_
#define SMART_PTR_H_
#include <cstdint>
template<typename T>
class SmartPtr {
private:
struct Ptr {
T* p_;
uint64_t count_;
Ptr(T* p) : p_{p}, count_{1} {}
~Ptr() { delete p_; }
};
public:
SmartPtr(T* p) : ptr_{new Ptr{p}} {}
~SmartPtr();
SmartPtr(const SmartPtr<T>& rhs);
SmartPtr(SmartPtr<T>&& rhs) =delete;
SmartPtr<T>& operator=(const SmartPtr<T>& rhs);
SmartPtr<T>& operator=(SmartPtr<T>&& rhs) =delete;
T& operator*() { return *ptr_->p_; }
T* operator->() { return ptr_->p_; }
uint64_t Count() const { return ptr_->count_; }
const T* Raw() const { return ptr_->p_; }
private:
Ptr* ptr_;
};
template<typename T>
SmartPtr<T>::~SmartPtr() {
if (!--ptr_->count_) {
delete ptr_;
}
ptr_ = nullptr;
}
template<typename T>
SmartPtr<T>::SmartPtr(const SmartPtr<T>& rhs) : ptr_{rhs.ptr_} {
++ptr_->count_;
}
template<typename T>
SmartPtr<T>& SmartPtr<T>::operator=(const SmartPtr<T>& rhs) {
if (this != &rhs) {
if (!--ptr_->count_) {
delete ptr_;
}
ptr_ = rhs.ptr_;
++ptr_->count_;
}
return *this;
}
#endif // SMART_PTR_H_
1 ответ
директива
Никогда не удаляйте специальные элементы перемещения.
В типичном коде (например, в вашем вопросе) есть две причины, чтобы удалить элементы перемещения. Одно из этих побуждений приводит к неправильному коду (как в вашем примере), а для другого побуждения удаление членов перемещения является излишним (не приносит никакого вреда или пользы).
Если у вас есть копируемый класс и вы не хотите перемещать элементы, просто не объявляйте их (что включает в себя их удаление). Удаленные участники все еще объявлены. Удаленные участники участвуют в разрешении перегрузки. Члены не присутствуют, не делают. Когда вы создаете класс с допустимым конструктором копирования и удаленным элементом перемещения, вы не можете вернуть его по значению из функции, поскольку разрешение перегрузки будет привязано к удаленному элементу перемещения.
Иногда люди хотят сказать: этот класс не является ни подвижным, ни копируемым. Правильно удалить и копию, и элементы перемещения. Однако достаточно просто удалить элементы копии (если элементы перемещения не объявлены). Объявленные (даже удаленные) члены копии запрещают компилятору объявлять элементы перемещения. Таким образом, в этом случае удаленные элементы перемещения просто избыточны.
Если вы объявляете удаленных участников перемещения, даже если вы выбрали случай, когда он избыточен и не является неправильным, каждый раз, когда кто-то читает ваш код, он должен заново обнаруживать, является ли ваш случай избыточным или неправильным. Облегчите чтение вашего кода и никогда не удаляйте перемещенные элементы.
Неверный случай:
struct CopyableButNotMovble
{
// ...
CopyableButNotMovble(const CopyableButNotMovble&);
CopyableButNotMovble& operator=(const CopyableButNotMovble&);
CopyableButNotMovble(CopyableButNotMovble&&) = delete;
CopyableButNotMovble& operator=(CopyableButNotMovble&&) = delete;
// ...
};
Вот пример кода, с которым вы, вероятно, ожидаете работать CopyableButNotMovble
но потерпит неудачу во время компиляции:
CopyableButNotMovble
get()
{
CopyableButNotMovble x;
return x;
}
int
main()
{
CopyableButNotMovble x = get();
}
test.cpp:22:26: error: call to deleted constructor of 'CopyableButNotMovble'
CopyableButNotMovble x = get();
^ ~~~~~
test.cpp:7:5: note: 'CopyableButNotMovble' has been explicitly marked deleted here
CopyableButNotMovble(CopyableButNotMovble&&) = delete;
^
1 error generated.
Правильный способ сделать это:
struct CopyableButNotMovble
{
// ...
CopyableButNotMovble(const CopyableButNotMovble&);
CopyableButNotMovble& operator=(const CopyableButNotMovble&);
// ...
};
Резервный корпус:
struct NeitherCopyableNorMovble
{
// ...
NeitherCopyableNorMovble(const NeitherCopyableNorMovble&) = delete;
NeitherCopyableNorMovble& operator=(const NeitherCopyableNorMovble&) = delete;
NeitherCopyableNorMovble(NeitherCopyableNorMovble&&) = delete;
NeitherCopyableNorMovble& operator=(NeitherCopyableNorMovble&&) = delete;
// ...
};
Более читаемый способ сделать это:
struct NeitherCopyableNorMovble
{
// ...
NeitherCopyableNorMovble(const NeitherCopyableNorMovble&) = delete;
NeitherCopyableNorMovble& operator=(const NeitherCopyableNorMovble&) = delete;
// ...
};
Это помогает, если вы практикуете всегда группировать все 6 ваших специальных членов в верхней части вашего объявления класса, в том же порядке, пропуская те, которые вы не хотите объявлять. Эта практика помогает читателям вашего кода быстро определить, что вы намеренно не объявили какого-либо конкретного специального члена.
Например, вот схема, которой я следую:
class X
{
// data members:
public:
// special members
~X();
X();
X(const X&);
X& operator=(const X&);
X(X&&);
X& operator=(X&&);
// Constructors
// ...
};