Como devo escrever o ISO C ++ Standard em conformidade com novos e personalizados operadores de exclusão?

Como devo escrever o padrão ISO C ++ em conformidade com new e personalizados operadores de delete ?

Isso é uma continuação do Overloading new e do delete no C ++ FAQ, sobrecarga do operador e seu acompanhamento. Por que um deve replace os novos operadores padrão e delete?

Seção 1: Escrevendo um new operador em conformidade com o padrão

  • Parte 1: Entendendo os requisitos para escrever um new operador personalizado
  • Parte 2: Entendendo os requisitos do new_handler
  • Parte 3: Entendendo os requisitos do cenário específico

Seção 2: Escrevendo um Operador de Exclusão em Conformidade Padrão

  • Implementando o operador de exclusão customizada

(Nota: Esta é uma input para o C ++ FAQ do Stack Overflow . Se você quiser criticar a idéia de fornecer um FAQ neste formulário, então o post no meta que iniciou tudo isso seria o lugar para fazer isso. essa questão é monitorada na sala de chat do C ++ , onde a ideia do FAQ começou em primeiro lugar, então é muito provável que sua resposta seja lida por aqueles que surgiram com a ideia.)
Nota: A resposta é baseada em aprendizados do More Effective C ++ de Scott Meyers e do padrão ISO C ++.

Parte I

Esta input FAQ C ++ explicou por que alguém pode querer sobrecarregar new operadores e delete para sua própria class. Este presente FAQ tenta explicar como se faz isso de uma maneira padronizada.

Implementando um new operador customizado

O padrão C ++ (§18.4.1.1) define operator new como:

 void* operator new (std::size_t size) throw (std::bad_alloc); 

O padrão C ++ especifica a semântica que as versões personalizadas desses operadores devem obedecer em §3.7.3 e §18.4.1

Vamos resumir os requisitos.

Requisito nº 1: deve alocar dinamicamente pelo menos bytes de size de memory e retornar um ponteiro para a memory alocada. Citação do padrão C ++, seção 3.7.4.1.3:

A function de alocação tenta alocar a quantidade solicitada de armazenamento. Se for bem sucedido, deve devolver o endereço do início de um bloco de armazenamento cujo comprimento em bytes seja pelo menos tão grande quanto o tamanho solicitado …

O padrão ainda impõe:

… O ponteiro retornado deve ser alinhado adequadamente para que possa ser convertido em um ponteiro de qualquer tipo de object completo e, em seguida, usado para acessar o object ou matriz no armazenamento alocado (até que o armazenamento seja desalocado explicitamente por uma chamada function de desalocação). Mesmo se o tamanho do espaço solicitado for zero, a solicitação poderá falhar. Se a solicitação for bem-sucedida, o valor retornado deverá ser um valor de ponteiro não nulo (4.10) p0 diferente de qualquer valor retornado anteriormente p1, a menos que o valor p1 tenha sido passado subseqüentemente para uma delete operador.

Isso nos dá outros requisitos importantes:

Requisito # 2: A function de alocação de memory que usamos (geralmente malloc() ou algum outro alocador customizado) deve retornar um ponteiro adequadamente alinhado à memory alocada, que pode ser convertida em um ponteiro de um tipo de object completo e usada para acessar o object .

Requisito nº 3: nosso operador personalizado new deve retornar um ponteiro legítimo mesmo quando zero bytes forem solicitados.

Um dos requisitos evidentes que podem ser inferidos do new protótipo é:

Requisito nº 4: se new não pode alocar memory dinâmica do tamanho solicitado, então deve lançar uma exceção do tipo std::bad_alloc .

Mas! Há mais do que aquilo que se encontra à vista: se você olhar mais de perto a new documentação do operador (a citação do padrão segue mais abaixo), ele afirma:

Se set_new_handler tiver sido usado para definir uma function new_handler , essa function new_handler é chamada pela definição padrão do operator new se não puder alocar o armazenamento solicitado por conta própria.

Para entender como nossos new personalizados precisam suportar esse requisito, devemos entender:

O que é o new_handler e o set_new_handler ?

new_handler é um typedef para um ponteiro para uma function que recebe e retorna nada, e set_new_handler é uma function que recebe e retorna um new_handler .

O parâmetro set_new_handler é um ponteiro para o operador da function new deve chamar se não puder alocar a memory solicitada. Seu valor de retorno é um ponteiro para a function de manipulador registrada anteriormente ou nulo se não houver nenhum manipulador anterior.

Um momento oportuno para um exemplo de código para esclarecer as coisas:

 #include  #include  // function to call if operator new can't allocate enough memory or error arises void outOfMemHandler() { std::cerr << "Unable to satisfy request for memory\n"; std::abort(); } int main() { //set the new_handler std::set_new_handler(outOfMemHandler); //Request huge memory size, that will cause ::operator new to fail int *pBigDataArray = new int[100000000L]; return 0; } 

No exemplo acima, o operator new (mais provável) não poderá alocar espaço para 100.000.000 de números inteiros, e a function outOfMemHandler() será chamada e o programa será interrompido após a emissão de uma mensagem de erro .

É importante observar aqui que quando o operator new não consegue atender a uma solicitação de memory, ele chama a function new-handler repetidamente até encontrar memory suficiente ou não há mais novos manipuladores. No exemplo acima, a menos que chamemos std::abort() , outOfMemHandler() seria chamado repetidamente . Portanto, o manipulador deve garantir que a próxima alocação seja bem-sucedida ou registrar outro manipulador ou não registrar nenhum manipulador ou não retornar (ou seja, encerrar o programa). Se não houver um novo manipulador e a alocação falhar, o operador lançará uma exceção.

Continuação 1


parte II

… contínuo

Dado o comportamento do operator new do exemplo, um new_handler bem projetado deve new_handler um dos seguintes new_handler :

Disponibilizar mais memory: Isso permite que a próxima tentativa de alocação de memory dentro do loop do operador new seja bem-sucedida. Uma maneira de implementar isso é alocar um grande bloco de memory na boot do programa e liberá-lo para uso no programa na primeira vez em que o novo manipulador for chamado.

Instalar um novo manipulador diferente: Se o novo manipulador atual não puder disponibilizar mais memory, e houver outro novo manipulador que possa, o novo manipulador atual poderá instalar o novo manipulador novo em seu lugar ( chamando set_new_handler ). Na próxima vez que o operador new chamar a function new-handler, ele terá o mais recentemente instalado.

(Uma variação deste tema é para um novo manipulador modificar seu próprio comportamento, então, da próxima vez que for invocado, ele fará algo diferente. Uma maneira de conseguir isso é fazer com que o novo manipulador modifique dados estáticos, específicos de namespace ou dados globais que afetam o comportamento do novo manipulador.)

Desinstale o novo manipulador: Isso é feito passando um ponteiro nulo para set_new_handler . Com nenhum novo manipulador instalado, o operator new lançará uma exceção ((conversível para) std::bad_alloc ) quando a alocação de memory não for bem-sucedida.

Lançar uma exceção conversível para std::bad_alloc . Essas exceções não serão detectadas pelo operator new , mas serão propagadas para o site que originou a solicitação de memory.

Não retorna: chamando abort ou exit .

Para implementar um new_handler específico da new_handler , temos que fornecer uma class com suas próprias versões de set_new_handler e operator new . O set_new_handler da class permite que os clientes especifiquem o novo manipulador para a class (exatamente como o padrão set_new_handler permite que os clientes especifiquem o novo manipulador global). O operator new da class operator new garante que o novo manipulador específico da class seja usado no lugar do novo manipulador global quando a memory para objects de class for alocada.


Agora que entendemos new_handler & set_new_handler melhor, podemos modificar o Requisito # 4 adequadamente como:

Requisito nº 4 (aprimorado):
Nosso operator new deve tentar alocar memory mais de uma vez, chamando a function new-handling após cada falha. A suposição aqui é que a nova function de manipulação pode ser capaz de fazer alguma coisa para liberar alguma memory. Somente quando o ponteiro para a function new-handling for null , o operator new lançará uma exceção.

Conforme prometido, a citação do Padrão:
Seção 3.7.4.1.3:

Uma function de alocação que não consegue alocar armazenamento pode invocar o new_handler instalado new_handler ( 18.4.2.2 ), se houver. [Nota: Uma function de alocação fornecida pelo programa pode obter o endereço do new_handler atualmente instalado usando a function set_new_handler ( 18.4.2.3 ).] Se uma function de alocação declarada com uma especificação de exceção vazia ( 15.4 ), throw() falhar alocar armazenamento, ele deve retornar um ponteiro nulo. Qualquer outra function de alocação que não aloque o armazenamento deve apenas indicar uma falha, std::bad_alloc uma exceção da class std::bad_alloc ( 18.4.2.1 ) ou uma class derivada de std::bad_alloc .

Armado com os requisitos nº 4 , vamos tentar o pseudo código para o nosso new operator :

 void * operator new(std::size_t size) throw(std::bad_alloc) { // custom operator new might take additional params(3.7.3.1.1) using namespace std; if (size == 0) // handle 0-byte requests { size = 1; // by treating them as } // 1-byte requests while (true) { //attempt to allocate size bytes; //if (the allocation was successful) //return (a pointer to the memory); //allocation was unsuccessful; find out what the current new-handling function is (see below) new_handler globalHandler = set_new_handler(0); set_new_handler(globalHandler); if (globalHandler) //If new_hander is registered call it (*globalHandler)(); else throw std::bad_alloc(); //No handler is registered throw an exception } } 

Continuação 2

Parte III

… contínuo

Note que não podemos obter o novo ponteiro de function de manipulador diretamente, temos que chamar set_new_handler para descobrir o que é. Isso é grosseiro, mas eficaz, pelo menos para o código de thread único. Em um ambiente multithread, provavelmente será necessário algum tipo de bloqueio para manipular com segurança as estruturas de dados (globais) por trás da nova function de manipulação. ( Mais citação / detalhes são bem-vindos sobre isso. )

Além disso, temos um loop infinito e a única maneira de sair do loop é que a memory seja alocada com sucesso ou que a nova function de manipulação faça uma das coisas que inferimos antes. A menos que o new_handler faça uma dessas coisas, esse loop dentro do new operador nunca terminará.

Uma advertência: Observe que o padrão ( §3.7.4.1.3 , citado acima) não diz explicitamente que o new operador sobrecarregado deve implementar um loop infinito, mas apenas diz que esse é o comportamento padrão. Portanto, esse detalhe está aberto à interpretação, mas a maioria dos compiladores ( GCC e Microsoft Visual C ++ ) implementam essa funcionalidade de loop (você pode compilar os exemplos de código fornecidos anteriormente). Além disso, uma vez que um autor de C ++ como Scott Meyers sugere essa abordagem, é bastante razoável.

Cenários especiais

Vamos considerar o seguinte cenário.

 class Base { public: static void * operator new(std::size_t size) throw(std::bad_alloc); }; class Derived: public Base { //Derived doesn't declare operator new }; int main() { // This calls Base::operator new! Derived *p = new Derived; return 0; } 

Como esta FAQ, explica, uma razão comum para escrever um gerenciador de memory personalizado é otimizar a alocação para objects de uma class específica, não para uma class ou qualquer uma de suas classs derivadas, o que basicamente significa que nosso operador new para a class Base é tipicamente sintonizado para objects de tamanho sizeof(Base) nada maior e nada menor.

No exemplo acima, devido à inheritance, a class derivada Derived herda o novo operador da class Base. Isso torna o operador de chamada novo em uma class base para alocar memory para um object de uma class derivada possível. A melhor maneira para o nosso operator new lidar com essa situação é desviar essas chamadas solicitando a quantidade “errada” de memory para o operador padrão novo, como este:

 void * Base::operator new(std::size_t size) throw(std::bad_alloc) { if (size != sizeof(Base)) // If size is "wrong,", that is, != sizeof Base class { return ::operator new(size); // Let std::new handle this request } else { //Our implementation } } 

Note que, a verificação do tamanho também incorpora nossa exigência # 3 . Isso ocorre porque todos os objects autônomos têm um tamanho diferente de zero em C ++, portanto sizeof(Base) nunca pode ser zero, portanto, se o tamanho for zero, a solicitação será encaminhada para ::operator new , e é garantido que ele manipulará de forma compatível com o padrão.

Citação: Do criador do próprio C ++, Dr. Bjarne Stroustrup.

Implementando um Operador de Exclusão Customizado

A biblioteca C ++ Standard ( §18.4.1.1 ) define o operator delete como:

 void operator delete(void*) throw(); 

Vamos repetir o exercício de reunir os requisitos para escrever nossa operator delete personalizada do operator delete :

Requisito nº 1: deve retornar void e seu primeiro parâmetro será void* . Um delete operator personalizado também pode ter mais de um parâmetro, mas precisamos apenas de um parâmetro para passar o ponteiro apontando para a memory alocada.

Citação do padrão C ++:

Seção §3.7.3.2.2:

“Cada function de desalocação deverá retornar void e seu primeiro parâmetro será nulo *. Uma function de desalocação pode ter mais de um parâmetro …..”

Requisito # 2: deve garantir que é seguro excluir um ponteiro nulo passado como argumento.

Citação do Padrão C ++: Seção §3.7.3.2.3:

O valor do primeiro argumento fornecido para uma das funções de desalocação fornecidas na biblioteca padrão pode ser um valor de ponteiro nulo; se assim for, a chamada para a function de desalocação não terá efeito. Caso contrário, o valor fornecido ao operator delete(void*) na biblioteca padrão deve ser um dos valores retornados por uma chamada anterior de operator new(size_t) ou operator new(size_t, const std::nothrow_t&) na biblioteca padrão , e o valor fornecido ao operator delete[](void*) na biblioteca padrão deve ser um dos valores retornados por uma invocação anterior de qualquer operator new[](size_t) ou operator new[](size_t, const std::nothrow_t&) na biblioteca padrão.

Requisito # 3: se o ponteiro que está sendo passado não é null , o delete operator deve desalocar a memory dinâmica alocada e atribuída ao ponteiro.

Citação do Padrão C ++: Seção §3.7.3.2.4:

Se o argumento dado a uma function de desalocação na biblioteca padrão for um ponteiro que não é o valor de ponteiro nulo (4.10), a function de desalocação desalocará o armazenamento referenciado pelo ponteiro, tornando inválidos todos os pointers referentes a qualquer parte do ponteiro. armazenamento desalocado.

Requisito nº 4: Além disso, como o nosso operador específico da class encaminha solicitações do tamanho “errado” para ::operator new , DEVEM enviar solicitações de exclusão “de tamanho incorreto” para ::operator delete .

Portanto, com base nos requisitos resumidos acima, há um pseudo código em conformidade padrão para um delete operator personalizado:

 class Base { public: //Same as before static void * operator new(std::size_t size) throw(std::bad_alloc); //delete declaration static void operator delete(void *rawMemory, std::size_t size) throw(); void Base::operator delete(void *rawMemory, std::size_t size) throw() { if (rawMemory == 0) { return; // No-Op is null pointer } if (size != sizeof(Base)) { // if size is "wrong," ::operator delete(rawMemory); //Delegate to std::delete return; } //If we reach here means we have correct sized pointer for deallocation //deallocate the memory pointed to by rawMemory; return; } };