Excluir isso é permitido?


232

É permitido delete this;se a instrução delete for a última instrução que será executada nessa instância da classe? É claro que tenho certeza de que o objeto representado pelo thisponteiro é newcriado.

Estou pensando em algo assim:

void SomeModule::doStuff()
{
    // in the controller, "this" object of SomeModule is the "current module"
    // now, if I want to switch over to a new Module, eg:

    controller->setWorkingModule(new OtherModule());

    // since the new "OtherModule" object will take the lead, 
    // I want to get rid of this "SomeModule" object:

    delete this;
}

Posso fazer isso?


13
O principal problema seria que, se você delete thiscriou um forte acoplamento entre a classe e o método de alocação usado para criar objetos dessa classe. Esse é um projeto de OO muito ruim, pois a coisa mais fundamental no OOP é criar classes autônomas que não sabem ou se importam com o que o chamador está fazendo. Portanto, uma classe projetada corretamente não deve saber ou se importar com como foi alocada. Se, por algum motivo, você precisar de um mecanismo tão peculiar, acho que um design melhor seria usar uma classe de wrapper em torno da classe real e deixar o wrapper lidar com a alocação.
Lundin

Você não pode excluir setWorkingModule?
Jimmy T.

Respostas:


238

O C ++ FAQ Lite possui uma entrada específica para este

Eu acho que essa citação resume bem

Desde que você tenha cuidado, não há problema em um objeto cometer suicídio (exclua isso).


15
O FQA correspondente também possui alguns comentários úteis: yosefk.com/c++fqa/heap.html#fqa-16.15 #
Alexandre C.

1
Por segurança, você pode usar destruidor particular no objeto original para garantir que não seja construído na pilha ou como parte de uma matriz ou vetor.
Cem Kalyoncu 29/09/2015

Definir 'cuidadoso'
CinCout 13/09

3
'Cuidado' é definido no artigo de FAQ vinculado. (Enquanto o link FQA principalmente rants - como quase tudo na mesma - o quão ruim C ++ é)
CharonX

88

Sim, delete this;definiu resultados, desde que (como você notou) assegure que o objeto foi alocado dinamicamente e (é claro) nunca tente usar o objeto depois que ele for destruído. Ao longo dos anos, muitas perguntas foram feitas sobre o que o padrão diz especificamente delete this;, em vez de excluir outro indicador. A resposta para isso é bastante curta e simples: não diz muito de nada. Apenas diz que deleteo operando deve ser uma expressão que designa um ponteiro para um objeto ou uma matriz de objetos. Ele entra em detalhes sobre coisas como descobrir qual (se houver) função de desalocação chamar para liberar a memória, mas a seção inteira em delete(§ [expr.delete]) não menciona delete this;especificamente nada. A seção sobre destruidores mencionadelete this em um só lugar (§ [class.dtor] / 13):

No ponto de definição de um destruidor virtual (incluindo uma definição implícita (15.8)), a função de desalocação sem matriz é determinada como se a expressão excluísse isso aparecendo em um destruidor não virtual da classe do destruidor (consulte 8.3.5 )

Isso tende a apoiar a ideia de que o padrão considera delete this;válido - se fosse inválido, seu tipo não seria significativo. Esse é o único lugar que o padrão menciona delete this;, até onde eu sei.

De qualquer forma, alguns consideram delete thisum truque desagradável e dizem a quem quiser ouvir que deve ser evitado. Um problema comumente citado é a dificuldade de garantir que os objetos da classe só sejam alocados dinamicamente. Outros o consideram um idioma perfeitamente razoável e o usam o tempo todo. Pessoalmente, estou em algum lugar no meio: raramente o uso, mas não hesite em fazê-lo quando parecer a ferramenta certa para o trabalho.

A primeira vez que você usa essa técnica é com um objeto que tem uma vida quase inteiramente própria. Um exemplo que James Kanze citou foi um sistema de cobrança / rastreamento no qual ele trabalhou para uma companhia telefônica. Quando você inicia uma ligação, algo toma nota disso e cria um phone_callobjeto. A partir desse momento, o phone_callobjeto lida com os detalhes da chamada telefônica (estabelecendo uma conexão quando você disca, adicionando uma entrada ao banco de dados para dizer quando a chamada foi iniciada, possivelmente conecte mais pessoas se você fizer uma chamada em conferência, etc.) Quando as últimas pessoas na ligação desligam, o phone_callobjeto faz a contabilidade final (por exemplo, adiciona uma entrada ao banco de dados para dizer quando você desligou, para que eles possam calcular quanto tempo durou a ligação) e depois se destrói. A vida útil dophone_callO objeto é baseado no momento em que a primeira pessoa inicia a chamada e quando as últimas saem - do ponto de vista do resto do sistema, é basicamente totalmente arbitrário, portanto você não pode vinculá-lo a nenhum escopo lexical no código , ou qualquer coisa nessa ordem.

Para qualquer pessoa que se preocupe com a confiabilidade desse tipo de codificação: se você ligar para, de ou através de quase qualquer parte da Europa, há uma boa chance de que ele seja tratado (pelo menos em parte) pelo código isso faz exatamente isso.


2
Obrigado, vou colocar em algum lugar da minha memória. Suponho que você defina os construtores e destruidores como privados e use algum método estático de fábrica para criar esses objetos.
Alexandre C.

@ Alexandre: Você provavelmente faria isso na maioria dos casos de qualquer maneira - eu não sei nem de perto todos os detalhes do sistema em que ele estava trabalhando, então não posso dizer com certeza.
Jerry Coffin

A maneira como costumo contornar o problema de como a memória foi alocada é incluir um bool selfDeleteparâmetro no construtor que é atribuído a uma variável de membro. É verdade que isso significa entregar ao programador corda suficiente para amarrar um laço nele, mas acho isso preferível a vazamentos de memória.
precisa saber é o seguinte

1
@ MBraedley: Eu fiz o mesmo, mas prefiro evitar o que me parece ser um clamor.
Jerry Coffin

Para qualquer um que possa se importar ... há uma boa chance de que ele esteja sendo tratado (pelo menos em parte) pelo código que faz exatamente this. Sim, o código está sendo tratado exatamente this. ;)
Galaxy

46

Se isso te assusta, há um truque perfeitamente legal:

void myclass::delete_me()
{
    std::unique_ptr<myclass> bye_bye(this);
}

Eu acho que delete thisé C ++ idiomático, e só apresento isso como uma curiosidade.

Há um caso em que essa construção é realmente útil - você pode excluir o objeto depois de lançar uma exceção que precisa de dados do membro. O objeto permanece válido até depois do lançamento.

void myclass::throw_error()
{
    std::unique_ptr<myclass> bye_bye(this);
    throw std::runtime_exception(this->error_msg);
}

Nota: se você estiver usando um compilador mais antigo que o C ++ 11, poderá usar em std::auto_ptrvez de std::unique_ptr, ele fará a mesma coisa.


Não consigo fazer isso compilar usando o c ++ 11, existem algumas opções especiais do compilador para isso? Também não requer a movimentação do ponteiro this?
coruja

@ Coruja não sabe o que quer dizer, funciona para mim: ideone.com/aavQUK . Criar um unique_ptrde outro unique_ptr requer uma movimentação, mas não de um ponteiro bruto. A menos que as coisas mudem no C ++ 17?
Mark Ransom

Ahh C ++ 14, é por isso. Preciso atualizar meu c ++ na minha caixa de desenvolvimento. Vou tentar novamente esta noite no meu sistema gentoo recentemente surgido!
Coruja #

25

Um dos motivos pelos quais o C ++ foi projetado foi facilitar a reutilização do código. Em geral, o C ++ deve ser escrito para que funcione se a classe é instanciada no heap, em uma matriz ou na pilha. "Excluir isso" é uma prática de codificação muito ruim, porque só funcionará se uma única instância for definida no heap; e é melhor não haver outra instrução de exclusão, que normalmente é usada pela maioria dos desenvolvedores para limpar a pilha. Isso também pressupõe que nenhum programador de manutenção no futuro curará um vazamento de memória percebido falsamente ao adicionar uma instrução de exclusão.

Mesmo que você saiba com antecedência que seu plano atual é alocar apenas uma única instância no heap, e se algum desenvolvedor de boa sorte surgir no futuro e decidir criar uma instância na pilha? Ou, se ele recortar e colar determinadas partes da classe em uma nova classe que ele pretende usar na pilha? Quando o código alcança "delete this", ele apaga e apaga, mas quando o objeto sai do escopo, ele chama o destruidor. O destruidor tentará excluí-lo novamente e, em seguida, você receberá uma mangueira. No passado, fazer algo assim iria estragar não apenas o programa, mas o sistema operacional e o computador precisariam ser reiniciados. De qualquer forma, isso NÃO é altamente recomendado e quase sempre deve ser evitado. Eu teria que estar desesperado, seriamente engessado,


7
+1. Não consigo entender por que você foi derrotado. "O C ++ deve ser escrito para que funcione se a classe é instanciada na pilha, em uma matriz ou na pilha" é um conselho muito bom.
Joh

1
Você pode simplesmente agrupar o objeto que deseja excluir a si mesmo em uma classe especial que exclui o objeto e depois a si próprio, e usar esta técnica para impedir a alocação de pilha: stackoverflow.com/questions/124880/… Há momentos em que não há realmente um alternativa viável. Eu apenas usei essa técnica para excluir automaticamente um thread iniciado por uma função DLL, mas a função DLL deve retornar antes que o thread termine.
Felix Dombek

Você não pode programar de tal maneira que alguém que apenas copie e cole com o seu código acabe abusando dele de qualquer maneira
Jimmy T.

22

É permitido (apenas não use o objeto depois disso), mas eu não escreveria esse código na prática. Eu acho que delete thisdeve aparecer apenas em funções que chamou releaseou Releasee se parece com: void release() { ref--; if (ref<1) delete this; }.


Que é exatamente uma vez em cada projeto meu ... :-)
cmaster - Reintegrar monica

15

Bem, na construção do Component Object Model (COM) delete thispode ser uma parte do Releasemétodo chamado sempre que você deseja liberar o objeto adquirido:

void IMyInterface::Release()
{
    --instanceCount;
    if(instanceCount == 0)
        delete this;
}

8

Este é o idioma principal para objetos contados por referência.

A contagem de referência é uma forma forte de coleta de lixo determinística - garante que os objetos gerenciem sua própria vida útil, em vez de depender de ponteiros 'inteligentes' etc. para fazer isso por eles. O objeto subjacente só é acessado através de ponteiros inteligentes "Referência", projetados para que os ponteiros aumentem e diminuam um número inteiro do membro (a contagem de referência) no objeto real.

Quando a última referência cair da pilha ou for excluída, a contagem de referência será zero. O comportamento padrão do seu objeto será uma chamada para "excluir isso" na coleta de lixo - as bibliotecas que eu escrevo fornecem uma chamada "CountIsZero" virtual protegida na classe base, para que você possa substituir esse comportamento por coisas como armazenamento em cache.

A chave para tornar isso seguro não é permitir que os usuários acessem o CONSTRUTOR do objeto em questão (proteja-o), mas faça com que eles chamem algum membro estático - o FACTORY - como "static Reference CreateT (...)". Dessa forma, você sabe com certeza que eles sempre são construídos com "novos" comuns e que nenhum ponteiro bruto está disponível; portanto, "excluir isso" nunca explodirá.


Por que você não pode simplesmente ter uma classe (singleton) "alocador / coletor de lixo", uma interface através da qual toda a alocação é feita e permitir que essa classe lide com toda a contagem de referência dos objetos alocados? Em vez de forçar os próprios objetos a se incomodarem com as tarefas de coleta de lixo, algo completamente não relacionado ao objetivo designado.
Lundin

1
Você também pode proteger o destruidor para proibir alocações estáticas e de pilha do seu objeto.
Game_Overture 27/01

7

Você pode fazer isso. No entanto, você não pode atribuir isso. Portanto, a razão pela qual você declara fazer isso "Quero mudar de opinião" parece muito questionável. O melhor método, na minha opinião, seria o objeto que mantém a visão substituir essa visão.

Claro, você está usando objetos RAII e, portanto, não precisa chamar a exclusão ... certo?


4

Essa é uma pergunta antiga, respondida, mas o @Alexandre perguntou "Por que alguém iria querer fazer isso?", E pensei em fornecer um exemplo de uso que estou considerando esta tarde.

Código legado. Usa ponteiros nus Obj * obj com um objeto de exclusão no final.

Infelizmente, às vezes, não muitas vezes, preciso manter o objeto vivo por mais tempo.

Estou pensando em torná-lo um ponteiro inteligente contado como referência. Mas haveria muito código para alterar, se eu fosse usar em ref_cnt_ptr<Obj>qualquer lugar. E se você misturar Obj * nu e ref_cnt_ptr, poderá excluir o objeto implicitamente quando o último ref_cnt_ptr desaparecer, mesmo que o Obj * ainda esteja vivo.

Então, eu estou pensando em criar um explícito_delete_ref_cnt_ptr. Ou seja, um ponteiro contado de referência em que a exclusão é feita apenas em uma rotina de exclusão explícita. Usá-lo no local em que o código existente conhece a vida útil do objeto, bem como no meu novo código que mantém o objeto vivo por mais tempo.

Incrementar e decrementar a contagem de referência à medida que explícita_delete_ref_cnt_ptr é manipulada.

Mas NÃO libera quando a contagem de referência é vista como zero no destruidor explícito_delete_ref_cnt_ptr.

Liberando apenas quando a contagem de referência é zero em uma operação explícita de exclusão. Por exemplo, em algo como:

template<typename T> class explicit_delete_ref_cnt_ptr { 
 private: 
   T* ptr;
   int rc;
   ...
 public: 
   void delete_if_rc0() {
      if( this->ptr ) {
        this->rc--;
        if( this->rc == 0 ) {
           delete this->ptr;
        }
        this->ptr = 0;
      }
    }
 };

OK, algo assim. É um pouco incomum ter um tipo de ponteiro contado por referência para não excluir automaticamente o objeto apontado no destruidor rc'ed ptr. Mas parece que isso pode tornar a mistura de ponteiros nus e ponteiros rc'ed um pouco mais segura.

Mas até agora não há necessidade de excluir isso.

Mas então me ocorreu: se o objeto apontado, o apontador, sabe que está sendo contado como referência, por exemplo, se a contagem estiver dentro do objeto (ou em alguma outra tabela), a rotina delete_if_rc0 pode ser um método do objeto de ponta, não o ponteiro (inteligente).

class Pointee { 
 private: 
   int rc;
   ...
 public: 
   void delete_if_rc0() {
        this->rc--;
        if( this->rc == 0 ) {
           delete this;
        }
      }
    }
 };

Na verdade, ele não precisa ser um método membro, mas pode ser uma função livre:

map<void*,int> keepalive_map;
template<typename T>
void delete_if_rc0(T*ptr) {
        void* tptr = (void*)ptr;
        if( keepalive_map[tptr] == 1 ) {
           delete ptr;
        }
};

(BTW, eu sei que o código não está certo - ele se torna menos legível se eu adicionar todos os detalhes, por isso estou deixando assim.)


0

Excluir isso é legal, desde que o objeto esteja na pilha. Você precisaria exigir que o objeto fosse apenas heap. A única maneira de fazer isso é proteger o destruidor - dessa forma, a exclusão pode ser chamada SOMENTE da classe, portanto você precisaria de um método que garanta a exclusão

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.