Qual é a utilidade do `enable_shared_from_this`?


349

Corri enable_shared_from_thisao ler os exemplos do Boost.Asio e depois de ler a documentação, ainda estou perdida pela maneira como isso deve ser usado corretamente. Alguém pode me dar um exemplo e uma explicação de quando usar essa classe faz sentido.

Respostas:


362

Ele permite que você obtenha uma shared_ptrinstância válida para thisquando tudo o que você tem é this. Sem ele, você não teria nenhuma maneira de obter um shared_ptrpara this, a menos que você já teve um como um membro. Este exemplo da documentação do 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
}

O método f()retorna um válido shared_ptr, mesmo que não tivesse instância de membro. Observe que você não pode simplesmente fazer isso:

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_ptr<Y>(this);
    }
}

O ponteiro compartilhado que isso retornou terá uma contagem de referência diferente da "adequada" e uma delas acabará perdendo e mantendo uma referência pendente quando o objeto for excluído.

enable_shared_from_thistornou-se parte do padrão C ++ 11. Você também pode obtê-lo de lá, bem como do impulso.


202
+1. O ponto principal é que a técnica "óbvia" de apenas retornar shared_ptr <Y> (isto) está quebrada, porque isso acaba criando vários objetos shared_ptr distintos com contagens de referência separadas. Por esse motivo, você nunca deve criar mais de um shared_ptr a partir do mesmo ponteiro bruto .
Jrandom_hacker 3/04/09

3
Deve-se observar que no C ++ 11 e posterior , é perfeitamente válido usar um std::shared_ptrconstrutor em um ponteiro bruto, se herdar de std::enable_shared_from_this. Não sei se a semântica do Boost foi atualizada para suportar isso.
Matthew

6
@MatthewHolder Você tem uma cotação para isso? No cppreference.com, li "Construir um std::shared_ptrpara um objeto que já é gerenciado por outro std::shared_ptrnão consultará a referência fraca armazenada internamente e, portanto, levará a um comportamento indefinido". ( pt.cppreference.com/w/cpp/memory/enable_shared_from_this )
Thorbjørn Lindeijer

5
Por que você não pode simplesmente fazer shared_ptr<Y> q = p?
Dan M.

2
@ ThorbjørnLindeijer, você está certo, é C ++ 17 e posterior. Algumas implementações seguiram a semântica do C ++ 16 antes de ser lançada. O manuseio adequado para C ++ 11 a C ++ 14 deve ser usado std::make_shared<T>.
Matthew

198

do artigo do Dr. Dobbs sobre indicadores fracos, acho que este exemplo é mais fácil de entender (fonte: http://drdobbs.com/cpp/184402026 ):

... código como este não funcionará corretamente:

int *ip = new int;
shared_ptr<int> sp1(ip);
shared_ptr<int> sp2(ip);

Nenhum dos dois shared_ptrobjetos conhece o outro, portanto, ambos tentarão liberar o recurso quando forem destruídos. Isso geralmente leva a problemas.

Da mesma forma, se uma função membro precisar de um shared_ptrobjeto que possua o objeto em que está sendo chamada, ela não poderá simplesmente criar um objeto em tempo real:

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 tem o mesmo problema que o exemplo anterior, embora de uma forma mais sutil. Quando é construído, o shared_ptobjeto r sp1possui o recurso recém-alocado. O código dentro da função de membro S::dangerousnão sabe sobre esse shared_ptrobjeto, portanto, o shared_ptrobjeto que ele retorna é distinto sp1. Copiar o novo shared_ptrobjeto para sp2não ajuda; quando sp2sai do escopo, ele libera o recurso e, quando sp1sai do escopo, libera o recurso novamente.

A maneira de evitar esse problema é usar o modelo de classe enable_shared_from_this. O modelo usa um argumento de tipo de modelo, que é o nome da classe que define o recurso gerenciado. Essa classe, por sua vez, deve ser derivada publicamente do modelo; como isso:

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;
}

Ao fazer isso, lembre-se de que o objeto no qual você chama shared_from_thisdeve pertencer a um shared_ptrobjeto. Isso não vai funcionar:

int main()
{
   S *p = new S;
   shared_ptr<S> sp2 = p->not_dangerous();     // don't do this
}

15
Obrigado, isso ilustra o problema que está sendo resolvido melhor do que a resposta atualmente aceita.
goertzenator

2
+1: boa resposta. Como um aparte, em vez de shared_ptr<S> sp1(new S);ser preferível usá-lo shared_ptr<S> sp1 = make_shared<S>();, consulte por exemplo stackoverflow.com/questions/18301511/…
Arun

4
Tenho certeza de que a última linha deve ser lida, shared_ptr<S> sp2 = p->not_dangerous();porque a armadilha aqui é que você deve criar um shared_ptr da maneira normal antes de ligar shared_from_this()pela primeira vez! É realmente fácil errar! Antes do C ++ 17, é UB chamar shared_from_this()antes que exatamente um shared_ptr tenha sido criado da maneira normal: auto sptr = std::make_shared<S>();ou shared_ptr<S> sptr(new S());. Felizmente, a partir do C ++ 17, isso será lançado.
AnorZaken 01/09


2
@AnorZaken Bom argumento. Seria útil se você tivesse enviado uma solicitação de edição para fazer essa correção. Acabei de o fazer. A outra coisa útil seria o pôster não escolher nomes de métodos subjetivos e sensíveis ao contexto!
Underscore_d

30

Aqui está minha explicação, do ponto de vista de porcas e parafusos (a resposta principal não 'clicou' comigo). * Observe que esse é o resultado da investigação da origem do shared_ptr e do enable_shared_from_this fornecido com o Visual Studio 2012. Talvez outros compiladores implementem o enable_shared_from_this de maneira diferente ... *

enable_shared_from_this<T> adiciona um privado weak_ptr<T> instância à Tqual contém a ' única contagem de referência verdadeira ' para a instância de T.

Então, quando você cria um shared_ptr<T> para um novo T *, o ponto fraco_ptr interno do T * é inicializado com uma contagem de ref 1. O novo shared_ptrbasicamente se baseia nisso weak_ptr.

TEm seguida, em seus métodos, pode chamar shared_from_thispara obter uma instância shared_ptr<T>disso para a mesma contagem de referência armazenada internamente . Dessa forma, você sempre tem um lugar onde T*a contagem de ref é armazenada, em vez de ter várias shared_ptrinstâncias que não se conhecem e cada uma pensa que é a shared_ptrresponsável pela contagem de ref Te excluí-la quando a ref -count chega a zero.


11
Isso está correto, e a parte realmente importante é So, when you first create...porque isso é um requisito (como você diz que o fraca_ptr não é inicializado até você passar o ponteiro de objetos para um compartilhado_ptrador!) E esse requisito é onde as coisas podem dar terrivelmente errado se você estiver descuidado. Se você não criar um shared_ptr antes de ligar, shared_from_thisreceberá o UB - da mesma forma, se você criar mais de um shared_ptr, também receberá o UB. Você precisa, de alguma forma, criar um shared_ptr exatamente uma vez.
AnorZaken

2
Em outras palavras, toda a idéia de enable_shared_from_thisé frágil, pois o objetivo é obter um shared_ptr<T>de a T*, mas, na realidade, quando você recebe um ponteiro T* t, geralmente não é seguro assumir que algo já está sendo compartilhado ou não, e fazer o palpite errado é UB.
AnorZaken

" internal fraco_ptr é inicializado com uma contagem decrescente de 1 " ptr fraco para T não possui proprietário inteligente para T. Um ptr fraco é um ref inteligente proprietário para informações suficientes para criar um proprietário que é uma "cópia" de outro proprietário. Um ptr fraco não tem contagem de ref. Ele tem acesso a uma contagem de referências, como todas as referências próprias.
precisa

3

Observe que o uso de um boost :: intrusive_ptr não sofre esse problema. Geralmente, é uma maneira mais conveniente de contornar esse problema.


Sim, mas enable_shared_from_thispermite que você trabalhe com uma API que aceite especificamente shared_ptr<>. Na minha opinião, essa API geralmente está fazendo errado (como é melhor deixar que algo mais alto na pilha possua a memória), mas se você for forçado a trabalhar com essa API, essa é uma boa opção.
precisa saber é o seguinte

2
Melhor permanecer dentro do padrão o máximo que puder.
Sergei

3

É exatamente o mesmo no c ++ 11 e posterior: é para permitir a capacidade de retornar thiscomo um ponteiro compartilhado, poisthis fornece um ponteiro bruto.

em outras palavras, permite transformar código como este

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

private:

    Node * m_parent = nullptr;
};           

nisso:

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;
};           

Isso funcionará apenas se esses objetos forem sempre gerenciados por um shared_ptr. Você pode alterar a interface para garantir que seja o caso.
precisa

11
Você está absolutamente correto @curiousguy. Isso é desnecessário. Também gosto de digitar todos os meus shared_ptr para melhorar a legibilidade ao definir minhas APIs públicas. Por exemplo, em vez de std::shared_ptr<Node> getParent const(), eu normalmente o exporia NodePtr getParent const(). Se você absolutamente precisa acessar o ponteiro bruto interno (melhor exemplo: lidando com uma biblioteca C), é std::shared_ptr<T>::getisso que eu odeio mencionar porque eu tenho esse acessador de ponteiro bruto usado muitas vezes pelo motivo errado.
Mkiasson

-3

Outra maneira é adicionar um weak_ptr<Y> m_stubmembro ao class Y. Então escreva:

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

Útil quando você não pode alterar a classe da qual você está derivando (por exemplo, estendendo a biblioteca de outras pessoas). Não se esqueça de inicializar o membro, por exemplo,m_stub = shared_ptr<Y>(this) , , é válido mesmo durante um construtor.

Tudo bem se houver mais stubs como este na hierarquia de herança, isso não impedirá a destruição do objeto.

Editar: Conforme indicado corretamente pelo usuário nobar, o código destruirá o objeto Y quando a atribuição for concluída e as variáveis ​​temporárias forem destruídas. Portanto, minha resposta está incorreta.


4
Se a sua intenção aqui é produzir um shared_ptr<>que não apague seu pontapé, isso é um exagero. Você pode simplesmente dizer return shared_ptr<Y>(this, no_op_deleter);onde no_op_deleterestá um objeto de função unária que não aceita Y*e não faz nada.
John Zwinck

2
Parece improvável que esta seja uma solução funcional. m_stub = shared_ptr<Y>(this)irá construir e destruir imediatamente um shared_ptr temporário disso. Quando essa instrução terminar, thisserá excluída e todas as referências subseqüentes ficarão pendentes.
Nobar

2
O autor reconhece que esta resposta está errada e provavelmente poderia excluí-la. Mas ele entrou pela última vez em 4,5 anos, por isso não é provável que o faça - alguém com poderes mais altos pode remover esse arenque vermelho?
21716 Tom Goodfellow

se você observar a implementação de enable_shared_from_this, ela mantém um weak_ptrde si (preenchido pelo ctor), retornado como shared_ptrquando você chama shared_from_this. Em outras palavras, você está duplicando o que enable_shared_from_thisjá fornece.
Mkiasson
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.