¿Cuál es la utilidad de `enable_shared_from_this`?

Resuelto fido asked hace 15 años • 6 respuestas

Me encontré enable_shared_from_thismientras 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?

fido avatar Apr 03 '09 08:04 fido
Aceptado

Le permite obtener una shared_ptrinstancia válida para this, cuando todo lo que tiene es this. Sin él, no tendrías forma de conseguir un shared_ptrto 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_thisse ha convertido en parte del estándar C++ 11. También puedes obtenerlo desde allí y desde Boost.

1800 INFORMATION avatar Apr 03 '2009 02:04 1800 INFORMATION

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_ptrobjetos 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_ptrobjeto 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_ptobjeto r sp1posee el recurso recién asignado. El código dentro de la función miembro S::dangerousno conoce ese shared_ptrobjeto, por lo que el shared_ptrobjeto que devuelve es distinto de sp1. Copiar el nuevo shared_ptrobjeto sp2no ayuda; cuando sp2salga del alcance, liberará el recurso y cuando sp1salga 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_thisdebe ser propiedad de un shared_ptrobjeto. Esto no funcionará:

int main()
{
   S *p = new S;
   shared_ptr<S> sp2 = p->not_dangerous();     // don't do this
}
Artashes Aghajanyan avatar Apr 05 '2011 07:04 Artashes Aghajanyan

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 Tla 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_ptrbásicamente vuelve a this weak_ptr.

TLuego, en sus métodos, puede llamar shared_from_thispara 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_ptrinstancias que no se conocen entre sí, y cada una piensa que es la shared_ptrencargada de contarlas Ty eliminarlas cuando su referencia. -el recuento llega a cero.

mackenir avatar Aug 01 '2012 12:08 mackenir

Hay un caso particular que encuentro enable_shared_from_thisextremadamente útil: la seguridad de subprocesos cuando se utiliza una devolución de llamada asincrónica.

Imagine que la clase Clienttiene 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_thispara 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_ptrrecuento de referencias, nos permite garantizar que el Clientobjeto no pueda destruirse mientras el código de devolución de llamada accede a sus miembros internos.

Tenga en cuenta que el Initmétodo está separado del constructor ya que el proceso de inicialización enable_shared_from_thisno 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.

Elad Maimoni avatar Aug 25 '2020 13:08 Elad Maimoni