¿Cuál es la utilidad de `enable_shared_from_this`?
Me encontré enable_shared_from_this
mientras leía los ejemplos de Boost.Asio y después de leer la documentación todavía no sé cómo se debe usar esto correctamente. ¿Puede alguien darme un ejemplo y una explicación de cuándo tiene sentido utilizar esta clase?
Le permite obtener una shared_ptr
instancia válida para this
, cuando todo lo que tiene es this
. Sin él, no tendrías forma de conseguir un shared_ptr
to this
, a menos que ya tengas uno como miembro. Este ejemplo de la documentación de impulso para enable_shared_from_this :
class Y: public enable_shared_from_this<Y>
{
public:
shared_ptr<Y> f()
{
return shared_from_this();
}
}
int main()
{
shared_ptr<Y> p(new Y);
shared_ptr<Y> q = p->f();
assert(p == q);
assert(!(p < q || q < p)); // p and q must share ownership
}
El método f()
devuelve un valor válido shared_ptr
, aunque no tenga una instancia miembro. Tenga en cuenta que no puede simplemente hacer esto:
class Y: public enable_shared_from_this<Y>
{
public:
shared_ptr<Y> f()
{
return shared_ptr<Y>(this);
}
}
El puntero compartido que esto devolvió tendrá un recuento de referencias diferente al "adecuado", y uno de ellos terminará perdiendo y manteniendo una referencia pendiente cuando se elimine el objeto.
enable_shared_from_this
se ha convertido en parte del estándar C++ 11. También puedes obtenerlo desde allí y desde Boost.
Del artículo del Dr. Dobbs sobre puntos débiles, creo que este ejemplo es más fácil de entender (fuente: http://drdobbs.com/cpp/184402026 ):
...código como este no funcionará correctamente:
int *ip = new int;
shared_ptr<int> sp1(ip);
shared_ptr<int> sp2(ip);
Ninguno de los dos shared_ptr
objetos conoce al otro, por lo que ambos intentarán liberar el recurso cuando sean destruidos. Eso suele generar problemas.
De manera similar, si una función miembro necesita un shared_ptr
objeto que sea propietario del objeto al que se llama, no puede simplemente crear un objeto sobre la marcha:
struct S
{
shared_ptr<S> dangerous()
{
return shared_ptr<S>(this); // don't do this!
}
};
int main()
{
shared_ptr<S> sp1(new S);
shared_ptr<S> sp2 = sp1->dangerous();
return 0;
}
Este código tiene el mismo problema que el ejemplo anterior, aunque de una forma más sutil. Cuando se construye, el shared_pt
objeto r sp1
posee el recurso recién asignado. El código dentro de la función miembro S::dangerous
no conoce ese shared_ptr
objeto, por lo que el shared_ptr
objeto que devuelve es distinto de sp1
. Copiar el nuevo shared_ptr
objeto sp2
no ayuda; cuando sp2
salga del alcance, liberará el recurso y cuando sp1
salga del alcance, liberará el recurso nuevamente.
La forma de evitar este problema es utilizar la plantilla de clase enable_shared_from_this
. La plantilla toma un argumento de tipo de plantilla, que es el nombre de la clase que define el recurso administrado. Esa clase, a su vez, debe derivarse públicamente de la plantilla; como esto:
struct S : enable_shared_from_this<S>
{
shared_ptr<S> not_dangerous()
{
return shared_from_this();
}
};
int main()
{
shared_ptr<S> sp1(new S);
shared_ptr<S> sp2 = sp1->not_dangerous();
return 0;
}
Cuando haga esto, tenga en cuenta que el objeto al que llama shared_from_this
debe ser propiedad de un shared_ptr
objeto. Esto no funcionará:
int main()
{
S *p = new S;
shared_ptr<S> sp2 = p->not_dangerous(); // don't do this
}
Aquí está mi explicación, desde una perspectiva práctica (la respuesta principal no me hizo clic). *Tenga en cuenta que este es el resultado de investigar la fuente deshared_ptr y enable_shared_from_this que viene con Visual Studio 2012. Quizás otros compiladores implementen enable_shared_from_this de manera diferente...*
enable_shared_from_this<T>
agrega una weak_ptr<T>
instancia privada a T
la que se guarda el ' recuento de una referencia verdadera ' para la instancia de T
.
Entonces, cuando creas por primera vez un shared_ptr<T>
en un nuevo T*, el débil_ptr interno de ese T* se inicializa con un refcount de 1. El nuevo shared_ptr
básicamente vuelve a this weak_ptr
.
T
Luego, en sus métodos, puede llamar shared_from_this
para obtener una instancia de shared_ptr<T>
eso que se remonta al mismo recuento de referencia almacenado internamente . De esta manera, siempre tendrá un lugar donde T*
se almacena el recuento de referencias en lugar de tener varias shared_ptr
instancias que no se conocen entre sí, y cada una piensa que es la shared_ptr
encargada de contarlas T
y eliminarlas cuando su referencia. -el recuento llega a cero.
Hay un caso particular que encuentro enable_shared_from_this
extremadamente útil: la seguridad de subprocesos cuando se utiliza una devolución de llamada asincrónica.
Imagine que la clase Client
tiene un miembro de tipo AsynchronousPeriodicTimer
:
struct AsynchronousPeriodicTimer
{
// call this periodically on some thread...
void SetCallback(std::function<void(void)> callback);
void ClearCallback(); // clears the callback
}
struct Client
{
Client(std::shared_ptr< AsynchronousPeriodicTimer> timer)
: _timer(timer)
{
_timer->SetCallback(
[this]
()
{
assert(this); // what if 'this' is already dead because ~Client() has been called?
std::cout << ++_counter << '\n';
}
);
}
~Client()
{
// clearing the callback is not in sync with the timer, and can actually occur while the callback code is running
_timer->ClearCallback();
}
int _counter = 0;
std::shared_ptr< AsynchronousPeriodicTimer> _timer;
}
int main()
{
auto timer = std::make_shared<AsynchronousPeriodicTimer>();
{
auto client = std::make_shared<Client>(timer);
// .. some code
// client dies here, there is a race between the client callback and the client destructor
}
}
La clase de cliente suscribe una función de devolución de llamada al temporizador periódico. Una vez que el objeto del cliente sale del alcance, existe una condición de carrera entre la devolución de llamada del cliente y el destructor del cliente. ¡La devolución de llamada se puede invocar con un puntero colgante!
La solución: usar enable_shared_from_this
para extender la vida útil del objeto mientras dure la invocación de devolución de llamada.
struct Client : std::enable_shared_from_this<Client>
{
Client(std::shared_ptr< AsynchronousPeriodicTimer> timer)
: _timer(timer)
{
}
void Init()
{
auto captured_self = weak_from_this(); // weak_ptr to avoid cyclic references with shared_ptr
_timer->SetCallback(
[captured_self]
()
{
if (auto self = captured_self.lock())
{
// 'this' is guaranteed to be non-nullptr. we managed to promote captured_self to a shared_ptr
std::cout << ++self->_counter << '\n';
}
}
);
}
~Client()
{
// the destructor cannot be called while the callback is running. shared_ptr guarantees this
_timer->ClearCallback();
}
int _counter = 0;
std::shared_ptr< AsynchronousPeriodicTimer> _timer;
}
El mecanismo de enable_shared_from_this
, combinado con la seguridad inherente de los subprocesos del std::shared_ptr
recuento de referencias, nos permite garantizar que el Client
objeto no pueda destruirse mientras el código de devolución de llamada accede a sus miembros internos.
Tenga en cuenta que el Init
método está separado del constructor ya que el proceso de inicialización enable_shared_from_this
no finaliza hasta que el constructor sale. De ahí el método adicional. Generalmente no es seguro suscribirse a una devolución de llamada asincrónica desde un constructor, ya que la devolución de llamada puede acceder a campos no inicializados.