c++ - ¿Debo eliminar el constructor de movimientos y la asignación de movimientos de un puntero inteligente?
c++11 smart-pointers (1)
Estoy implementando un puntero inteligente simple, que básicamente realiza un seguimiento de la cantidad de referencias a un puntero que maneja.
Sé que podría implementar la semántica de movimientos, pero no creo que tenga sentido, ya que copiar un puntero inteligente es muy barato. Especialmente teniendo en cuenta que introduce oportunidades para producir errores desagradables.
Aquí está mi código de C ++ 11 (omití algunos códigos no esenciales). Los comentarios generales son bienvenidos también.
#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_
Guía
Nunca borres los miembros especiales de movimiento.
En el código típico (como en su pregunta), existen dos motivaciones para eliminar los miembros del movimiento. Una de esas motivaciones produce un código incorrecto (como en su ejemplo), y para la otra motivación, la eliminación de los miembros del movimiento es redundante (no hace daño ni es bueno).
Si tiene una clase que se puede copiar y no desea mover miembros, simplemente no los declare (lo que incluye no eliminarlos). Los miembros eliminados siguen siendo declarados. Los miembros eliminados participan en la resolución de sobrecarga. Los miembros que no están presentes no lo hacen. Cuando crea una clase con un constructor de copia válido y un miembro de movimiento eliminado, no puede devolverlo por valor de una función porque la resolución de sobrecarga se vinculará al miembro de movimiento eliminado.
A veces la gente quiere decir: esta clase no es movible ni copiable. Es correcto eliminar tanto la copia como los miembros de movimiento. Sin embargo, solo eliminar los miembros de la copia es suficiente (siempre y cuando los miembros de movimiento no estén declarados). Los miembros de copia declarados (incluso eliminados) impiden que el compilador declare miembros de movimiento. Entonces, en este caso, los miembros eliminados del movimiento son simplemente redundantes.
Si declara miembros eliminados del movimiento, incluso si elige el caso en el que es redundante y no incorrecto, cada vez que alguien lea su código, deberá volver a descubrir si su caso es redundante o incorrecto. Hágalo más fácil para los lectores de su código y nunca elimine los miembros de movimiento.
El caso incorrecto:
struct CopyableButNotMovble
{
// ...
CopyableButNotMovble(const CopyableButNotMovble&);
CopyableButNotMovble& operator=(const CopyableButNotMovble&);
CopyableButNotMovble(CopyableButNotMovble&&) = delete;
CopyableButNotMovble& operator=(CopyableButNotMovble&&) = delete;
// ...
};
Aquí está el código de ejemplo que probablemente esperaba trabajar con CopyableButNotMovble
pero fallará en el momento de la compilación:
#include <algorithm>
#include <vector>
struct CopyableButNotMovble
{
// ...
CopyableButNotMovble(const CopyableButNotMovble&);
CopyableButNotMovble& operator=(const CopyableButNotMovble&);
CopyableButNotMovble(CopyableButNotMovble&&) = delete;
CopyableButNotMovble& operator=(CopyableButNotMovble&&) = delete;
CopyableButNotMovble(int);
// ...
friend bool operator<(CopyableButNotMovble const& x, CopyableButNotMovble const& y);
};
int
main()
{
std::vector<CopyableButNotMovble> v{3, 2, 1};
std::sort(v.begin(), v.end());
}
In file included from test.cpp:1:
algorithm:3932:17: error: no
matching function for call to ''swap''
swap(*__first, *__last);
^~~~
algorithm:4117:5: note: in
instantiation of function template specialization ''std::__1::__sort<std::__1::__less<CopyableButNotMovble,
CopyableButNotMovble> &, CopyableButNotMovble *>'' requested here
__sort<_Comp_ref>(__first, __last, __comp);
^
algorithm:4126:12: note: in
instantiation of function template specialization ''std::__1::sort<CopyableButNotMovble *,
std::__1::__less<CopyableButNotMovble, CopyableButNotMovble> >'' requested here
_VSTD::sort(__first, __last, __less<typename iterator_traits<_RandomAccessIterator>::value_type>());
^
...
(muchos mensajes de error desagradables desde lo más profundo de tu std :: lib)
La forma correcta de hacer esto es:
struct CopyableButNotMovble
{
// ...
CopyableButNotMovble(const CopyableButNotMovble&);
CopyableButNotMovble& operator=(const CopyableButNotMovble&);
// ...
};
El caso redundante:
struct NeitherCopyableNorMovble
{
// ...
NeitherCopyableNorMovble(const NeitherCopyableNorMovble&) = delete;
NeitherCopyableNorMovble& operator=(const NeitherCopyableNorMovble&) = delete;
NeitherCopyableNorMovble(NeitherCopyableNorMovble&&) = delete;
NeitherCopyableNorMovble& operator=(NeitherCopyableNorMovble&&) = delete;
// ...
};
La forma más legible de hacer esto es:
struct NeitherCopyableNorMovble
{
// ...
NeitherCopyableNorMovble(const NeitherCopyableNorMovble&) = delete;
NeitherCopyableNorMovble& operator=(const NeitherCopyableNorMovble&) = delete;
// ...
};
Es útil si realiza la práctica de agrupar siempre a los 6 miembros especiales cerca de la parte superior de la declaración de su clase, en el mismo orden, omitiendo a los que no desea declarar. Esta práctica facilita que los lectores de su código determinen rápidamente que usted no ha declarado intencionalmente a ningún miembro especial en particular.
Por ejemplo, aquí está el patrón que sigo:
class X
{
// data members:
public:
// special members
~X();
X();
X(const X&);
X& operator=(const X&);
X(X&&);
X& operator=(X&&);
// Constructors
// ...
};