É seguro excluir um ponteiro vazio?

96

Suponha que eu tenha o seguinte código:

void* my_alloc (size_t size)
{
   return new char [size];
}

void my_free (void* ptr)
{
   delete [] ptr;
}

Isso é seguro? Ou deve ptrser convertido para char*antes da exclusão?

An̲̳̳drew
fonte
Por que você mesmo está fazendo gerenciamento de memória? Que estrutura de dados você está criando? A necessidade de gerenciamento de memória explícita é muito rara em C ++; você geralmente deve usar classes que tratam disso para você a partir do STL (ou do Boost em uma pitada).
Brian
6
Apenas para as pessoas que estão lendo, eu uso variáveis ​​void * como parâmetros para minhas threads no win c ++ (consulte _beginthreadex). Normalmente, eles estão apontando de forma aguda para as classes.
ryansstack
3
Neste caso, é um wrapper de propósito geral para novo / exclusão, que pode conter estatísticas de rastreamento de alocação ou um pool de memória otimizado. Em outros casos, tenho visto ponteiros de objeto armazenados incorretamente como variáveis ​​de membro void * e excluídos incorretamente no destruidor sem lançar de volta para o tipo de objeto apropriado. Então, eu estava curioso sobre a segurança / armadilhas.
Retirado de
1
Para um wrapper de propósito geral para new / delete, você pode sobrecarregar os operadores new / delete. Dependendo de qual ambiente você usa, você provavelmente obtém ganchos no gerenciamento de memória para controlar as alocações. Se você acabar em uma situação em que não sabe o que está excluindo, tome isso como uma forte dica de que seu projeto está abaixo do ideal e precisa de refatoração.
Hans
38
Eu acho que há muito questionamento em vez de respondê-la. (Não só aqui, mas em todos os SO)
Petruza 07/12/2011

Respostas:

24

Depende de "seguro". Geralmente funciona porque as informações são armazenadas junto com o ponteiro sobre a alocação em si, de modo que o desalocador pode retorná-lo ao lugar certo. Nesse sentido, é "seguro", desde que seu alocador use tags de limite interno. (Muitos fazem.)

No entanto, conforme mencionado em outras respostas, excluir um ponteiro void não chamará destruidores, o que pode ser um problema. Nesse sentido, não é "seguro".

Não há nenhuma boa razão para fazer o que você está fazendo da maneira que está fazendo. Se você deseja escrever suas próprias funções de desalocação, pode usar modelos de função para gerar funções com o tipo correto. Um bom motivo para fazer isso é gerar alocadores de pool, que podem ser extremamente eficientes para tipos específicos.

Conforme mencionado em outras respostas, esse é um comportamento indefinido em C ++. Em geral, é bom evitar comportamentos indefinidos, embora o assunto em si seja complexo e repleto de opiniões conflitantes.

Christopher
fonte
9
Como essa resposta é aceita? Não faz sentido "deletar um ponteiro vazio" - segurança é um ponto discutível.
Kerrek SB
6
"Não há nenhuma boa razão para fazer o que você está fazendo da maneira que está fazendo." Essa é a sua opinião, não um fato.
rxantos
8
@rxantos Forneça um contra-exemplo em que fazer o que o autor da pergunta quer fazer é uma boa ideia em C ++.
Christopher
Acho que essa resposta é na verdade razoável, mas também acho que qualquer resposta a essa pergunta precisa pelo menos mencionar que esse é um comportamento indefinido.
Kyle Strand
@Christopher Tente escrever um único sistema coletor de lixo que não seja específico do tipo, mas simplesmente funcione. O fato de que sizeof(T*) == sizeof(U*)para todos T,Usugere que deve ser possível ter uma void *implementação de coletor de lixo não baseada em modelo . Mas então, quando o gc realmente tem que deletar / liberar um ponteiro exatamente esta questão surge. Para fazê-lo funcionar, você precisa de wrappers destruidores de função lambda (urgh) ou precisa de algum tipo de coisa dinâmica de "tipo como dados" que permite ir e vir entre um tipo e algo armazenável.
BitTickler
147

A exclusão por meio de um ponteiro nulo não é definida pelo padrão C ++ - consulte a seção 5.3.5 / 3:

Na primeira alternativa (deletar objeto), se o tipo estático do operando for diferente de seu tipo dinâmico, o tipo estático deve obrigatoriamente ser uma classe base do tipo dinâmico do operando e o tipo estático deve ter um destruidor virtual ou o comportamento é indefinido . Na segunda alternativa (excluir array), se o tipo dinâmico do objeto a ser excluído for diferente de seu tipo estático, o comportamento é indefinido.

E sua nota de rodapé:

Isso implica que um objeto não pode ser excluído usando um ponteiro do tipo void * porque não há objetos do tipo void

.


fonte
6
Tem certeza de que acertou a cotação certa? Acho que a nota de rodapé se refere a este texto: "Na primeira alternativa (deletar objeto), se o tipo estático do operando for diferente de seu tipo dinâmico, o tipo estático deve ser uma classe base do tipo dinâmico do operando e o estático tipo deve ter um destruidor virtual ou o comportamento é indefinido. Na segunda alternativa (delete array) se o tipo dinâmico do objeto a ser excluído for diferente de seu tipo estático, o comportamento é indefinido. " :)
Johannes Schaub - litb
2
Você está certo - atualizei a resposta. Eu não acho que isso negue o ponto básico?
Não, claro que não. Ainda diz que é UB. Ainda mais, agora ele afirma normativamente que excluir o vazio * é UB :)
Johannes Schaub - litb
Preencher a memória de endereço apontado de um ponteiro vazio com NULLalguma diferença para gerenciamento de memória de aplicativo?
artu-hnrq
25

Não é uma boa ideia e não é algo que você faria em C ++. Você está perdendo suas informações de tipo sem motivo.

Seu destruidor não será chamado nos objetos em seu array que você está excluindo ao chamá-lo para tipos não primitivos.

Em vez disso, você deve substituir new / delete.

Excluir o vazio * provavelmente irá liberar sua memória corretamente por acaso, mas é errado porque os resultados são indefinidos.

Se por algum motivo desconhecido para mim você precisar armazenar seu ponteiro em um vazio * e então liberá-lo, você deve usar malloc e free.

Brian R. Bondy
fonte
5
Você está certo sobre o destrutor não ser chamado, mas errado sobre o tamanho ser desconhecido. Se você der excluir um ponteiro que você tem de novo, ele não na verdade sabe o tamanho da coisa que está sendo excluído, inteiramente além do tipo. Como ele faz isso não é especificado pelo padrão C ++, mas vi implementações em que o tamanho é armazenado imediatamente antes dos dados para os quais o ponteiro retornado por 'novo' aponta.
KeyserSoze
Removida a parte sobre o tamanho, embora o padrão C ++ diga que é indefinido. Eu sei que malloc / free funcionaria para ponteiros void *.
Brian R. Bondy
Suponha que você não tenha um link da Web para a seção relevante do padrão? Eu sei que as poucas implementações de new / delete que eu olhei definitivamente funcionam corretamente sem conhecimento de tipo, mas admito que não olhei o que o padrão especifica. O IIRC C ++ originalmente exigia uma contagem de elementos de array ao excluir arrays, mas não faz mais com as versões mais recentes.
KeyserSoze
Consulte a resposta de @ Neil Butterworth. Sua resposta deve ser aceita em minha opinião.
Brian R. Bondy
2
@keysersoze: Geralmente não concordo com sua afirmação. Só porque alguma implementação armazenou o tamanho antes da memória alocada, não significa que seja uma regra.
13

Excluir um ponteiro vazio é perigoso porque os destruidores não serão chamados no valor para o qual ele realmente aponta. Isso pode resultar em vazamentos de memória / recursos em seu aplicativo.

JaredPar
fonte
1
char não tem um construtor / destruidor.
rxantos
9

A pergunta não faz sentido. Sua confusão pode ser em parte devido à linguagem desleixada que as pessoas costumam usar comdelete :

Você usa deletepara destruir um objeto que foi alocado dinamicamente. Faça isso, você forma uma expressão de exclusão com um ponteiro para esse objeto . Você nunca "apaga um ponteiro". O que você realmente faz é "deletar um objeto que é identificado por seu endereço".

Agora vemos por que a pergunta não faz sentido: um ponteiro vazio não é o "endereço de um objeto". É apenas um endereço, sem semântica. Ele pode ter vindo do endereço de um objeto real, mas que a informação é perdida, porque foi codificada no tipo do ponteiro originais. A única maneira de restaurar um ponteiro de objeto é lançar o ponteiro void de volta para um ponteiro de objeto (o que requer que o autor saiba o que o ponteiro significa). voidem si é um tipo incompleto e, portanto, nunca o tipo de um objeto, e um ponteiro vazio nunca pode ser usado para identificar um objeto. (Os objetos são identificados em conjunto por seu tipo e endereço.)

Kerrek SB
fonte
É certo que a pergunta não faz muito sentido sem qualquer contexto circundante. Alguns compiladores C ++ ainda compilarão alegremente esse código sem sentido (se eles se sentirem úteis, podem gritar um aviso sobre isso). Portanto, a pergunta foi feita para avaliar os riscos conhecidos de execução de código legado que contém esta operação imprudente: vai travar? vazar parte ou toda a memória da matriz de caracteres? algo mais específico da plataforma?
Retirou em
Obrigado pela resposta atenciosa. Votos positivos!
Retirou em
1
@Andrew: Temo que o padrão seja bastante claro sobre isso: "O valor do operando de deletepode ser um valor de ponteiro nulo, um ponteiro para um objeto não array criado por uma nova expressão anterior ou um ponteiro para um subobjeto que representa uma classe base de tal objeto. Caso contrário, o comportamento é indefinido. " Então, se um compilador aceita seu código sem um diagnóstico, não é nada além de um bug no compilador ...
Kerrek SB
1
@KerrekSB - Re não é nada além de um bug no compilador - eu discordo. O padrão diz que o comportamento é indefinido. Isso significa que o compilador / implementação pode fazer qualquer coisa e ainda estar em conformidade com o padrão. Se a resposta do compilador for dizer que você não pode deletar um ponteiro void *, tudo bem. Se a resposta do compilador for apagar o disco rígido, tudo bem. OTOH, se a resposta do compilador é não gerar nenhum diagnóstico, mas gerar um código que libere a memória associada a esse ponteiro, isso também está OK. Esta é uma maneira simples de lidar com essa forma de UB.
David Hammen
Apenas para acrescentar: não estou tolerando o uso de delete void_pointer. É um comportamento indefinido. Os programadores nunca devem invocar um comportamento indefinido, mesmo que a resposta pareça fazer o que o programador deseja que seja feito.
David Hammen
6

Se você realmente deve fazer isso, por que não cortar o homem médio (os newe deleteoperadores) e chamar o mundial operator newe operator deletediretamente? (Claro, se você está tentando instrumentar os operadores newe delete, na verdade deve reimplementar operator newe operator delete.)

void* my_alloc (size_t size)
{
   return ::operator new(size);
}

void my_free (void* ptr)
{
   ::operator delete(ptr);
}

Observe que malloc(), ao contrário , operator newgera std::bad_allocfalha (ou chama o new_handlerse um estiver registrado).

bk1e
fonte
Correto, pois char não possui um construtor / destruidor.
rxantos 01 de
5

Porque char não tem nenhuma lógica destruidora especial. ISTO não funcionará.

class foo
{
   ~foo() { printf("huzza"); }
}

main()
{
   foo * myFoo = new foo();
   delete ((void*)foo);
}

O d'ctor não será chamado.


fonte
5

Se você deseja usar void *, por que não usa apenas malloc / free? new / delete é mais do que apenas gerenciamento de memória. Basicamente, new / delete chama um construtor / destruidor e há mais coisas acontecendo. Se você apenas usar tipos integrados (como char *) e excluí-los por meio de void *, funcionaria, mas ainda não é recomendado. O resultado final é usar malloc / free se quiser usar void *. Caso contrário, você pode usar funções de modelo para sua conveniência.

template<typename T>
T* my_alloc (size_t size)
{
   return new T [size];
}

template<typename T>
void my_free (T* ptr)
{
   delete [] ptr;
}

int main(void)
{
    char* pChar = my_alloc<char>(10);
    my_free(pChar);
}
jovem
fonte
Eu não escrevi o código no exemplo - tropecei neste padrão sendo usado em alguns lugares, curiosamente misturando gerenciamento de memória C / C ++, e estava me perguntando quais eram os perigos específicos.
Retirado em
Escrever C / C ++ é uma receita para o fracasso. Quem escreveu isso deveria ter escrito um ou outro.
David Thornley
2
@David Isso é C ++, não C / C ++. C não tem modelos, nem usa new e delete.
rxantos
5

Muita gente já comentou dizendo que não, não é seguro deletar um void pointer. Eu concordo com isso, mas também gostaria de acrescentar que se você estiver trabalhando com ponteiros nulos para alocar matrizes contíguas ou algo semelhante, você pode fazer isso newpara poder usar deletecom segurança (com, ahem , um pouco de trabalho extra). Isso é feito alocando um ponteiro vazio para a região de memória (chamado de 'arena') e, em seguida, fornecendo o ponteiro para a nova arena. Consulte esta seção nas Perguntas frequentes sobre C ++ . Esta é uma abordagem comum para implementar pools de memória em C ++.

Paul Morie
fonte
0

Dificilmente há uma razão para fazer isso.

Em primeiro lugar, se você não sabe o tipo dos dados, e tudo o que sabe é que são void*, então você realmente deveria tratar esses dados como um blob sem tipo de dados binários ( unsigned char*) e usar malloc/ freepara lidar com eles . Isso é necessário às vezes para coisas como dados de forma de onda e similares, onde você precisa passar void*ponteiros para C apis. Isso é bom.

Se você fazer saber o tipo de dados (ou seja, tem um ctor / dtor), mas por algum motivo você acabou com um void*ponteiro (por qualquer motivo que você tem) , então você realmente deve lançá-lo de volta para o tipo de conhecê-lo para seja , e convoque delete-o.

bobobobo
fonte
0

Usei void *, (também conhecido como tipos desconhecidos) em minha estrutura durante a reflexão de código e outros feitos de ambigüidade e, até agora, não tive problemas (vazamento de memória, violações de acesso, etc.) de nenhum compilador. Apenas avisos devido à operação não ser padrão.

Faz sentido excluir um desconhecido (vazio *). Certifique-se de que o ponteiro segue estas diretrizes ou pode deixar de fazer sentido:

1) O ponteiro desconhecido não deve apontar para um tipo que possui um desconstrutor trivial e, portanto, quando lançado como um ponteiro desconhecido, NUNCA DEVE SER EXCLUÍDO. Exclua o ponteiro desconhecido APÓS colocá-lo de volta no tipo ORIGINAL.

2) A instância está sendo referenciada como um ponteiro desconhecido na memória limite da pilha ou limite da pilha? Se o ponteiro desconhecido fizer referência a uma instância na pilha, ele NUNCA DEVE SER EXCLUÍDO!

3) Você tem 100% de certeza de que o ponteiro desconhecido é uma região de memória válida? Não, então NUNCA DEVE SER DELIMITADO!

Ao todo, há muito pouco trabalho direto que pode ser feito usando um tipo de ponteiro desconhecido (void *). No entanto, indiretamente, o vazio * é um grande recurso para os desenvolvedores C ++ confiarem quando a ambigüidade dos dados é necessária.

zackery.fix
fonte
0

Se você quiser apenas um buffer, use malloc / free. Se você deve usar new / delete, considere uma classe de wrapper trivial:

template<int size_ > struct size_buffer { 
  char data_[ size_]; 
  operator void*() { return (void*)&data_; }
};

typedef sized_buffer<100> OpaqueBuffer; // logical description of your sized buffer

OpaqueBuffer* ptr = new OpaqueBuffer();

delete ptr;
Sanjaya R
fonte
0

Para o caso particular de char.

char é um tipo intrínseco que não possui um destruidor especial. Portanto, os argumentos de vazamento são discutíveis.

sizeof (char) é geralmente um, então também não há argumento de alinhamento. No caso de uma plataforma rara onde o sizeof (char) não é um, eles alocam memória alinhada o suficiente para seu char. Portanto, o argumento do alinhamento também é discutível.

malloc / free seria mais rápido neste caso. Mas você perde std :: bad_alloc e tem que verificar o resultado de malloc. Chamar os novos operadores globais e excluir pode ser melhor, pois ignora o intermediário.

Rxantos
fonte
1
" sizeof (char) é geralmente um " sizeof (char) é SEMPRE um
curioso
Não até recentemente (2019) as pessoas pensam que newrealmente está definido para jogar. Isso não é verdade. É compilador e dependente da chave do compilador. Consulte por exemplo as /GX[-] enable C++ EH (same as /EHsc)chaves MSVC2019 . Também em sistemas embarcados, muitos optam por não pagar taxas de desempenho por exceções C ++. Portanto, a frase que começa com "Mas você perdeu std :: bad_alloc ..." é questionável.
BitTickler