Por que o destruidor não é chamado na exclusão do operador?

16

Eu tentei chamar ::deleteuma aula operator deletesobre isso. Mas o destruidor não é chamado.

Eu defini uma classe MyClassque operator deletefoi sobrecarregada. O global operator deletetambém está sobrecarregado. O sobrecarregado operator deletede MyClasschamará o global sobrecarregado operator delete.

class MyClass
{
public:
    MyClass() { printf("Constructing MyClass...\n"); }
    virtual ~MyClass() { printf("Destroying MyClass...\n"); }

    void* operator new(size_t size)
    {
        printf("Newing MyClass...\n");
        void* p = ::new MyClass();
        printf("End of newing MyClass...\n");
        return p;
    }

    void operator delete(void* p)
    {
        printf("Deleting MyClass...\n");
        ::delete p;    // Why is the destructor not called here?
        printf("End of deleting MyClass...\n");
    }
};

void* operator new(size_t size)
{
    printf("Global newing...\n");
    return malloc(size);
}

void operator delete(void* p)
{
    printf("Global deleting...\n");
    free(p);
}

int main(int argc, char** argv)
{
    MyClass* myClass = new MyClass();
    delete myClass;

    return EXIT_SUCCESS;
}

A saída é:

Newing MyClass...
Global newing...
Constructing MyClass...
End of newing MyClass...
Constructing MyClass...
Destroying MyClass...
Deleting MyClass...
Global deleting...
End of deleting MyClass...

Real:

Há apenas uma chamada para o destruidor antes de chamar a sobrecarga operator deletede MyClass.

Esperado:

Existem duas chamadas para o destruidor. Um antes de chamar o sobrecarregado operator deletede MyClass. Outro antes de chamar o global operator delete.

expinc
fonte
6
MyClass::operator new()deve alocar memória bruta, de (pelo menos) sizebytes. Não deve tentar construir completamente uma instância de MyClass. O construtor de MyClassé executado depois MyClass::operator new(). Em seguida, a deleteexpressão main()chama o destruidor e libera a memória (sem chamar o destruidor novamente). A ::delete pexpressão não possui informações sobre o tipo de objeto papontado para, uma vez que pé a void *, portanto, não pode invocar o destruidor.
Peter
Relacionados: stackoverflow.com/a/8918942/845092
Mooing Duck
2
As respostas já dadas a você estão corretas, mas eu me pergunto: por que você está tentando substituir new e delete? O caso de uso típico é implementar o gerenciamento de memória personalizado (GC, memória que não vem do malloc () padrão, etc.). Talvez você esteja usando a ferramenta errada para o que está tentando alcançar.
noamtm
2
::delete p;provoca um comportamento indefinido, pois o tipo de *pnão é o mesmo que o tipo de objeto que está sendo eliminada (nem uma classe base com destrutor virtual)
MM
@MM Os principais compiladores apenas o alertam no máximo, então eu não percebi que void*o operando é explicitamente mal formado. [expr.delete] / 1 : " O operando deve ser um ponteiro para o tipo de objeto ou de classe. [...] Isso implica que um objeto não pode ser excluído usando um ponteiro do tipo void porque void não é um tipo de objeto. * "@OP Alterei minha resposta.
Walnut #

Respostas:

17

Você está usando mal operator newe operator delete. Esses operadores são funções de alocação e desalocação. Eles não são responsáveis ​​por construir ou destruir objetos. Eles são responsáveis ​​apenas por fornecer a memória na qual o objeto será colocado.

As versões globais dessas funções são ::operator newe ::operator delete. ::newe ::deletesão novas / delete-expression, assim como são new/ delete, diferentes daquelas ::newe ::deleteignoram sobrecargas operator new/ classes específicas operator delete.

O novo / delete-expression constrói / destrói e aloca / desaloca (chamando o apropriado operator newou operator deleteantes da construção ou após a destruição).

Como sua sobrecarga é responsável apenas pela parte de alocação / desalocação, ela deve chamar ::operator newe, em ::operator deletevez de ::newe ::delete.

O deletein delete myClass;é responsável por chamar o destruidor.

::delete p;não chama o destruidor porque ptem tipo void*e, portanto, a expressão não pode saber qual destruidor chamar. Provavelmente chamará seu substituído ::operator deletepara desalocar a memória, embora o uso de um void*operando como para uma expressão de exclusão esteja mal formado (veja a edição abaixo).

::new MyClass();chama seu substituído ::operator newpara alocar memória e constrói um objeto nele. O ponteiro para esse objeto é retornado quanto void*à nova expressão em MyClass* myClass = new MyClass();, que então constrói outro objeto nessa memória, encerrando a vida útil do objeto anterior sem chamar seu destruidor.


Editar:

Graças ao comentário de @ MM sobre a questão, percebi que um void*operando as ::deleteé realmente mal formado. ( [expr.delete] / 1 ) No entanto, os principais compiladores parecem ter decidido apenas avisar sobre isso, não erro. Antes de ficar mal formado, usando ::deleteum void*comportamento já indefinido, consulte esta pergunta .

Portanto, seu programa está mal formado e você não tem garantia de que o código realmente faça o que eu descrevi acima se ele ainda conseguiu compilar.


Conforme apontado por @SanderDeDycker abaixo da resposta, você também tem um comportamento indefinido porque, ao construir outro objeto na memória que já contém um MyClassobjeto sem chamar primeiro o destruidor desse objeto, você está violando [basic.life] / 5, que o proíbe se o programa depende dos efeitos colaterais do destruidor. Nesse caso, a printfdeclaração no destruidor tem esse efeito colateral.

noz
fonte
O uso indevido visa verificar como esse operador funciona. No entanto, obrigado pela sua resposta. Parece a única resposta que resolve meu problema.
expinc
13

Suas sobrecargas específicas da classe são feitas incorretamente. Isso pode ser visto em sua saída: o construtor é chamado duas vezes!

Na classe específica operator new, chame o operador global diretamente:

return ::operator new(size);

Da mesma forma, no específico da classe operator delete, faça:

::operator delete(p);

Consulte a operator newpágina de referência para mais detalhes.

Sander De Dycker
fonte
Eu sei que o construtor é chamado duas vezes chamando :: new no operador new e quero dizer isso. Minha pergunta é por que o destruidor não é chamado ao chamar :: delete no operador delete?
expinc 25/10/19
11
@expinc: Chamar intencionalmente o construtor pela segunda vez sem chamar primeiro o destruidor é uma péssima ideia. Para destruidores não triviais (como o seu), você está até se aventurando em um território de comportamento indefinido (se depender dos efeitos colaterais do destruidor, o que você faz) - ref. [vida básica] §5 . Não faça isso.
Sander De Dycker 25/10/19
1

Consulte Referência de CPP :

operator delete, operator delete[]

Desaloca o armazenamento alocado anteriormente por uma correspondência operator new. Essas funções de desalocação são chamadas por expressões de exclusão e novas expressões para desalocar a memória após a destruição (ou falha na construção) de objetos com duração de armazenamento dinâmico. Eles também podem ser chamados usando a sintaxe de chamada de função regular.

Excluir (e novo) são responsáveis ​​apenas pela parte 'gerenciamento de memória'.

Portanto, é claro e esperado que o destruidor seja chamado apenas uma vez - para limpar a instância do objeto. Seria chamado duas vezes, todo destruidor teria que verificar se já havia sido chamado.

Mario a colher
fonte
11
O operador de exclusão ainda deve chamar implicitamente o destruidor, como você pode ver em seus próprios logs, mostrando o destruidor após a exclusão da instância. O problema aqui é que sua substituição de exclusão de classe está chamando :: delete, o que leva à sua substituição global de exclusão. Essa substituição de exclusão global apenas libera memória, de modo a não reinvocar o destruidor.
Pickle Rick (
A referência diz claramente que delete é chamado de APÓS objeto desconstrução
Mario The Spoon
Sim, a exclusão global é chamada após a exclusão da classe. Há duas substituições aqui.
Pickle Rick (
2
@PickleRick - embora seja verdade que uma expressão de exclusão chame um destruidor (supondo um ponteiro para um tipo com um destruidor) ou um conjunto de destruidores (forma de matriz), uma operator delete()função não é a mesma coisa que uma expressão de exclusão. O destruidor é chamado antes da operator delete()função ser chamada.
Peter
11
Ajudaria se você adicionasse um cabeçalho ao qoute. Atualmente, não está claro a que se refere. "Dellocates storage ..." - quem desaloca armazenamento?
idclev 463035818