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

349

Me encontré enable_shared_from_thismientras leía los ejemplos de Boost.Asio y después de leer la documentación todavía estoy perdido por cómo se debe usar correctamente. ¿Puede alguien darme un ejemplo y una explicación de cuándo usar esta clase tiene sentido?

fido
fuente

Respuestas:

362

Le permite obtener una shared_ptrinstancia válida para this, cuando todo lo que tiene es this. Sin él, no tendría forma de obtener un shared_ptra this, a menos que ya tuviera 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 válido shared_ptr, a pesar de que no tenía 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 este devuelto tendrá un recuento de referencia diferente del "correcto", y uno de ellos terminará perdiendo y manteniendo una referencia colgante cuando se elimine el objeto.

enable_shared_from_thisse ha convertido en parte del estándar C ++ 11. También puede obtenerlo desde allí, así como desde boost.

1800 INFORMACIÓN
fuente
202
+1. El punto clave es que la técnica "obvia" de simplemente devolver shared_ptr <Y> (esto) se rompe, porque esto termina creando múltiples objetos distintos shared_ptr con recuentos de referencia separados. Por este motivo, nunca debe crear más de un shared_ptr desde el mismo puntero sin formato .
j_random_hacker 03 de
3
Cabe señalar que en C ++ 11 y versiones posteriores , es perfectamente válido usar un std::shared_ptrconstructor en un puntero sin formato si hereda de std::enable_shared_from_this. No sé si la semántica de Boost se actualizó para respaldar esto.
Matthew
66
@MatthewHolder ¿Tiene una cotización para esto? En cppreference.com leí "Construir std::shared_ptrun objeto que ya está administrado por otro std::shared_ptrno consultará la referencia débil almacenada internamente y, por lo tanto, conducirá a un comportamiento indefinido". ( en.cppreference.com/w/cpp/memory/enable_shared_from_this )
Thorbjørn Lindeijer
55
¿Por qué no puedes simplemente hacer shared_ptr<Y> q = p?
Dan M.
2
@ ThorbjørnLindeijer, tienes razón, es C ++ 17 y posterior. Algunas implementaciones siguieron la semántica de C ++ 16 antes de su lanzamiento. Se debe utilizar el manejo adecuado para C ++ 11 a C ++ 14 std::make_shared<T>.
Matthew
198

del artículo del Dr. Dobbs sobre punteros débiles, creo que este ejemplo es más fácil de entender (fuente: http://drdobbs.com/cpp/184402026 ):

... un 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 generalmente lleva a problemas.

Del mismo modo, si una función miembro necesita un shared_ptrobjeto que posee el objeto al que se está llamando, no puede 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 en 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 sp1. Copiar el nuevo shared_ptrobjeto a sp2no ayuda; cuando está sp2fuera de alcance, liberará el recurso, y cuando sp1salga del alcance, volverá a liberar el recurso.

La forma de evitar este problema es usar 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 debe, a su vez, derivarse públicamente de la plantilla; Me gusta 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
fuente
15
Gracias, esto ilustra que el problema se resuelve mejor que la respuesta actualmente aceptada.
Goertzenator
2
+1: buena respuesta. Por otro lado, en lugar de shared_ptr<S> sp1(new S);usarlo shared_ptr<S> sp1 = make_shared<S>();, puede preferir usar stackoverflow.com/questions/18301511/…
Arun
44
¡Estoy bastante seguro de que la última línea debería leerse shared_ptr<S> sp2 = p->not_dangerous();porque el problema aquí es que debes crear un shared_ptr de la manera normal antes de llamar shared_from_this()la primera vez! ¡Esto es realmente fácil de equivocarse! Antes de C ++ 17, UB debe llamar shared_from_this()antes de que se haya creado exactamente un shared_ptr de la manera normal: auto sptr = std::make_shared<S>();o shared_ptr<S> sptr(new S());. Afortunadamente, desde C ++ 17 en adelante, arrojará.
AnorZaken
2
@AnorZaken Buen punto. Hubiera sido útil si hubiera enviado una solicitud de edición para hacer esa corrección. Lo acabo de hacer. ¡La otra cosa útil hubiera sido que el póster no eligiera nombres de métodos subjetivos y sensibles al contexto!
underscore_d
30

Aquí está mi explicación, desde una perspectiva de tuercas y pernos (la respuesta principal no 'hizo clic' conmigo). * Tenga en cuenta que este es el resultado de investigar el origen de shared_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>privada añade un weak_ptr<T>ejemplo de Tlo que sostiene el ' verdadero contador de referencia ' para la instancia de T.

Entonces, cuando creas un shared_ptr<T>nuevo en un nuevo T *, el débil_ptr interno de ese T * se inicializa con un recuento de 1. El nuevo shared_ptrbásicamente retrocede en esto weak_ptr.

Tpuede, entonces, en sus métodos, llamar shared_from_thispara obtener una instancia de shared_ptr<T>ese respaldo en el mismo recuento de referencia almacenado internamente . De esta manera, siempre tiene 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 son las shared_ptrencargadas de contarlas Ty eliminarlas cuando su referencia -cuenta llega a cero.

mackenir
fuente
1
Esto es correcto, y la parte realmente importante es So, when you first create...porque es un requisito (ya que usted dice que el débil_ptr no se inicializa hasta que pasa el puntero de los objetos a un controlador_compartido) y este requisito es donde las cosas pueden salir terriblemente mal si está no es cuidadoso. Si no crea shared_ptr antes de llamar shared_from_this, obtendrá UB; del mismo modo, si crea más de shared_ptr, también obtendrá UB. Tiene que asegurarse de alguna manera de crear un shared_ptr exactamente una vez.
AnorZaken
2
En otras palabras, toda la idea de enable_shared_from_thises frágil para empezar, ya que el punto es poder obtener un shared_ptr<T>de a T*, pero en realidad cuando obtienes un puntero T* t, generalmente no es seguro asumir nada acerca de que ya se ha compartido o no, y hacer una suposición equivocada es UB.
AnorZaken
" interno débil_ptr se inicializa con un recuento de 1 " débil ptr a T no son propietarios de ptr inteligentes a T. Un ptr débil no tiene recuento de ref. Tiene acceso a un recuento de referencia, como todos los propietarios de referencia.
curioso
3

Tenga en cuenta que el uso de boost :: intrusive_ptr no sufre este problema. Esta suele ser una forma más conveniente de solucionar este problema.

blais
fuente
Sí, pero le enable_shared_from_thispermite trabajar con una API que acepta específicamente shared_ptr<>. En mi opinión, dicha API suele estar haciendo mal (ya que es mejor dejar que algo más alto en la pila posea la memoria), pero si se ve obligado a trabajar con una API de este tipo, esta es una buena opción.
cdunn2001
2
Es mejor mantenerse dentro del estándar tanto como pueda.
Sergei
3

Es exactamente lo mismo en c ++ 11 y versiones posteriores: es para permitir la posibilidad de regresar thiscomo un puntero compartido, ya que thisle da un puntero sin formato.

en otras palabras, te permite activar código como este

class Node {
public:
    Node* getParent const() {
        if (m_parent) {
            return m_parent;
        } else {
            return this;
        }
    }

private:

    Node * m_parent = nullptr;
};           

dentro de esto:

class Node : std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getParent const() {
        std::shared_ptr<Node> parent = m_parent.lock();
        if (parent) {
            return parent;
        } else {
            return shared_from_this();
        }
    }

private:

    std::weak_ptr<Node> m_parent;
};           
mchiasson
fuente
Esto solo funcionará si estos objetos siempre son administrados por a shared_ptr. Es posible que desee cambiar la interfaz para asegurarse de que sea así.
curioso
1
Tienes toda la razón @curiousguy. Esto es evidente. También me gusta escribir todo mi shared_ptr para mejorar la legibilidad al definir mis API públicas. Por ejemplo, en lugar de std::shared_ptr<Node> getParent const(), normalmente lo expondría como en su NodePtr getParent const()lugar. Si realmente necesita acceder al puntero sin formato interno (el mejor ejemplo: tratar con una biblioteca C), hay std::shared_ptr<T>::getalgo que odio mencionar porque he usado este descriptor de acceso sin formato demasiadas veces por la razón equivocada.
mchiasson
-3

Otra forma es agregar un weak_ptr<Y> m_stubmiembro al class Y. Luego escribir:

shared_ptr<Y> Y::f()
{
    return m_stub.lock();
}

Útil cuando no puede cambiar la clase de la que deriva (por ejemplo, ampliar la biblioteca de otras personas). No olvide inicializar el miembro, por ejemplo m_stub = shared_ptr<Y>(this), por , es válido incluso durante un constructor.

Está bien si hay más trozos como este en la jerarquía de herencia, no evitará la destrucción del objeto.

Editar: como lo señaló correctamente el usuario nobar, el código destruiría el objeto Y cuando finalice la asignación y se destruyan las variables temporales. Por lo tanto mi respuesta es incorrecta.

PetrH
fuente
44
Si su intención aquí es producir una shared_ptr<>que no elimine a su puntero, esto es exagerado. Simplemente puede decir return shared_ptr<Y>(this, no_op_deleter);dónde no_op_deleterestá un objeto de función unaria tomando Y*y sin hacer nada.
John Zwinck
2
Parece poco probable que esta sea una solución que funcione. m_stub = shared_ptr<Y>(this)construirá e inmediatamente destruirá un shared_ptr temporal a partir de esto. Cuando termine esta declaración, thisse eliminará y todas las referencias posteriores estarán colgando.
nobar
2
El autor reconoce que esta respuesta es incorrecta, por lo que probablemente podría eliminarla. Pero la última vez que inició sesión fue de 4.5 años, por lo que no es probable que lo haga. ¿Podría alguien con poderes superiores eliminar este arenque rojo?
Tom Goodfellow
si observa la implementación de enable_shared_from_this, mantiene una parte weak_ptrde sí misma (poblada por el ctor), devuelta como shared_ptrcuando llama shared_from_this. En otras palabras, está duplicando lo que enable_shared_from_thisya proporciona.
mchiasson