Sobrecarga em C ++. Parte III Sobrecarregando novas instruções / exclusão


Continuamos a série "C ++, cavando em profundidade". O objetivo desta série é informar o máximo possível sobre os vários recursos do idioma, possivelmente bastante especiais. Este artigo é sobre sobrecarga do operador new/delete. Este é o terceiro artigo da série, o primeiro dedicado a funções e modelos de sobrecarga, localizado aqui , o segundo dedicado a operadores de sobrecarga, localizado aqui . Este artigo conclui uma série de três artigos sobre sobrecarga em C ++.


Índice


Índice
  1. new/delete
    1.1.
    1.2.
    1.3.
  2. new/delete
    2.1.
    2.2.
      2.2.1. new/delete
      2.2.2. new/delete
      2.2.3.
      2.2.4.
      2.2.5. operator delete()
  3. new/delete
  4.
  5. -
  

1. Formulários padrão de operadores novos / excluídos


C ++ suporta várias opções de operador new/delete. Eles podem ser divididos em padrão básico, padrão adicional e personalizado. Esta seção e a seção 2 discutem formulários padrão; os formulários personalizados serão discutidos na seção 3.

1.1 Formulários padrão básicos


As principais formas padrão de operadores new/deleteusadas ao criar e excluir um objeto e uma matriz do tipo são Tas seguintes:

new T(/*   */)
new T[/*   */]
delete ptr;
delete[] ptr;

O trabalho deles pode ser descrito da seguinte forma. Quando o operador é chamado new, a memória é alocada primeiro para o objeto. Se a seleção for bem sucedida, o construtor é chamado. Se o construtor lança uma exceção, a memória alocada é liberada. Quando o operador é chamado, deletetudo acontece na ordem inversa: primeiro, o destruidor é chamado e a memória é liberada. O destruidor não deve lançar exceções.

Quando o operadornew[]usada para criar uma matriz de objetos, a memória é alocada primeiro para toda a matriz. Se a seleção for bem-sucedida, o construtor padrão (ou outro construtor, se houver um inicializador) será chamado para cada elemento da matriz começando do zero. Se algum construtor lança uma exceção, para todos os elementos da matriz criados, o destruidor é chamado na ordem inversa da chamada do construtor, a memória alocada é liberada. Para excluir uma matriz, você deve chamar o operador delete[]e, para todos os elementos da matriz, o destruidor é chamado na ordem inversa do construtor e a memória alocada é liberada.

Atenção! É necessário chamar a forma correta do operadordeletedependendo se um único objeto ou matriz é excluído. Essa regra deve ser observada rigorosamente, caso contrário, você poderá obter um comportamento indefinido, ou seja, tudo pode acontecer: vazamento de memória, falha etc. Veja [Meyers1] para detalhes.

Na descrição acima, é necessário um esclarecimento. Para os chamados tipos triviais (tipos internos, estruturas no estilo C), o construtor padrão não pode ser chamado e o destruidor não faz nada em nenhum caso.

As funções de alocação de memória padrão, quando não é possível atender à solicitação, lançam uma exceção de tipo std::bad_alloc. Mas essa exceção pode ser detectada; para isso, você precisa instalar o interceptador global chamando a função set_new_handler(), para obter mais detalhes, consulte [Meyers1].

Qualquer forma de operadordeleteaplique com segurança ao ponteiro nulo.

Ao criar uma matriz com um operador, o new[]tamanho pode ser definido como zero.

Ambas as formas do operador newpermitem o uso de inicializadores entre chaves.

new int{42}
new int[8]{1,2,3,4}

1.2 Formulários padrão adicionais


Ao conectar o arquivo de cabeçalho <new>, mais 4 formulários de operador padrão ficam disponíveis new:

new(ptr) T(/*  */);
new(ptr) T[/*   */];
new(std::nothrow) T(/*   */);
new(std::nothrow) T[/*   */];

Os dois primeiros são chamados newde posicionamento sem alocação new. Um argumento ptré um ponteiro para uma região da memória que é grande o suficiente para armazenar uma instância ou matriz. Além disso, a área de memória deve ter um alinhamento apropriado. Esta versão do operador newnão aloca memória; fornece apenas uma chamada ao construtor. Portanto, esta opção permite separar as fases de alocação de memória e inicialização de objetos. Esse recurso é usado ativamente em contêineres padrão. O operador deletepara objetos criados dessa maneira não pode, é claro, ser chamado. Para excluir um objeto, você deve chamar diretamente o destruidor e liberar a memória de uma maneira que dependa do método de alocação de memória.

As duas segundas opções são chamadas de operador não lançando exceções new(nothrow new) e diferem no fato de que, se for impossível atender à solicitação, elas retornam nullptr, mas não lançam uma exceção de tipo std::bad_alloc. A exclusão de um objeto ocorre usando o operador principal delete. Essas opções são consideradas obsoletas e não recomendadas para uso.

1.3 Alocação de memória e funções livres


Formas padrão de operadores new/deleteusam as seguintes funções de alocação e desalocação:

void* operator new(std::size_t size);
void operator delete(void* ptr);
void* operator new[](std::size_t size);
void operator delete[](void* ptr);
void* operator new(std::size_t size, void* ptr);
void* operator new[](std::size_t size, void* ptr);
void* operator new(std::size_t size, const std::nothrow_t& nth);
void* operator new[](std::size_t size, const std::nothrow_t& nth);

Essas funções são definidas no espaço para nome global. As funções de alocação de memória para as instruções newdo host não fazem nada e simplesmente retornam ptr.

O C ++ 17 suportava formas adicionais de funções de alocação e desalocação de memória, indicando alinhamento. Aqui estão alguns deles:

void* operator new (std::size_t size, std::align_val_t al);
void* operator new[](std::size_t size, std::align_val_t al);

Esses formulários não são diretamente acessíveis ao usuário, eles são usados ​​pelo compilador para objetos cujos requisitos de alinhamento são superiores __STDCPP_DEFAULT_NEW_ALIGNMENT__; portanto, o principal problema é que o usuário não os oculta acidentalmente (consulte a seção 2.2.1). Lembre-se de que no C ++ 11 tornou-se possível definir explicitamente o alinhamento dos tipos de usuário.

struct alignas(32) X { /* ... */ };

2. Sobrecarregando os formulários padrão dos operadores new / delete


A sobrecarga das formas padrão de operadores new/deleteconsiste na definição de funções definidas pelo usuário para alocar e liberar memória cujas assinaturas coincidem com as padrão. Essas funções podem ser definidas no espaço para nome global ou em uma classe, mas não em um espaço para nome que não seja global. A função de alocação de memória para uma instrução de host padrão newnão pode ser definida no espaço para nome global. Após essa definição, os operadores correspondentes new/deleteos usarão, não os padrão.

2.1 Sobrecarga no espaço de nomes global


Suponha, por exemplo, em um módulo em um espaço para nome global que funções definidas pelo usuário sejam definidas:

void* operator new(std::size_t size)
{
// ...
}

void operator delete(void* ptr)
{
// ...
}

Nesse caso, haverá realmente uma substituição (substituição) das funções padrão para alocar e liberar memória para todas as chamadas do operador new/deletepara quaisquer classes (incluindo as padrão) em todo o módulo. Isso pode levar ao caos completo. Observe que o mecanismo de substituição descrito é um mecanismo especial implementado apenas para este caso, e não um mecanismo geral do C ++. Nesse caso, ao implementar as funções do usuário para alocar e liberar memória, torna-se impossível chamar as funções padrão correspondentes, elas ficam completamente ocultas (o operador ::não ajuda) e, quando você tenta chamá-las, ocorre uma chamada recursiva à função do usuário.

Função definida pelo espaço de nomes global

void* operator new(std::size_t size, const std::nothrow_t& nth)
{
// ...
}

Ele também substituirá o padrão, mas haverá menos problemas em potencial, porque o operador que não lança exceções newé raramente usado. Mas o formulário padrão também não está disponível.

A mesma situação com funções para matrizes.

A sobrecarga de instruções new/deleteno namespace global é altamente desencorajada.

2.2 Sobrecarga de classe


A sobrecarga de operadores new/deleteem uma classe é desprovida das desvantagens descritas acima. A sobrecarga é efetiva apenas ao criar e excluir instâncias da classe correspondente, independentemente do contexto de chamada de operadores new/delete. Ao implementar funções definidas pelo usuário para alocar e liberar memória usando o operador, ::você pode acessar as funções padrão correspondentes. Considere um exemplo.

class X
{
// ...
public:
    void* operator new(std::size_t size)
    {
        std::cout << "X new\n";
        return ::operator new(size);
    }

    void operator delete(void* ptr)
    {
        std::cout << "X delete\n";
        ::operator delete(ptr);
    }

    void* operator new[](std::size_t size)
    {
        std::cout << "X new[]\n";
        return ::operator new[](size);
    }

    void operator delete[](void* ptr)
    {
        std::cout << "X delete[]\n";
        ::operator delete[](ptr);
    }
};

Neste exemplo, o rastreamento é simplesmente adicionado às operações padrão. Agora, em termos new X()e new X[N]usará essas funções para alocar e liberar memória.

Essas funções são formalmente estáticas e podem ser declaradas como static. Mas, essencialmente, são instâncias, com a chamada da função operator new(), a criação da instância começa e a chamada da função operator delete()completa sua exclusão. Essas funções nunca são chamadas para outras tarefas. Além disso, como será mostrado abaixo, a função operator delete()é essencialmente virtual. Portanto, é mais correto declará-los sem static.

2.2.1 Acesso a formulários padrão de operadores novos / excluídos


Os operadores new/deletepodem ser usados ​​com um operador de resolução de escopo adicional, por exemplo ::new(p) X(). Nesse caso, a função operator new()definida na classe será ignorada e o padrão correspondente será usado. Da mesma maneira, você pode usar o operador delete.

2.2.2 Ocultando outras formas de operadores new / delete


Se agora, para a classe X, tentamos usar ou não exceções new, obtemos um erro. O fato é que a função operator new(std::size_t size)oculta outras formas operator new(). O problema pode ser resolvido de duas maneiras. No primeiro, você precisa adicionar as opções apropriadas à classe (essas opções devem simplesmente delegar a operação da função padrão). No segundo, você precisa usar um operador newcom um operador de resolução de escopo, por exemplo ::new(p) X().

2.2.3 Recipientes padrão


Se tentarmos colocar as instâncias Xem algum contêiner padrão, por exemplo std::vector<X>, veremos que nossas funções não são usadas para alocar e liberar memória. O fato é que todos os contêineres padrão têm seu próprio mecanismo para alocar e liberar memória (uma classe de alocador especial, que é um parâmetro de modelo do contêiner) e usam um operador de posicionamento para inicializar os elementos new.

2.2.4 Herança


Funções para alocar e liberar memória são herdadas. Se essas funções forem definidas na classe base, mas não na classe derivada, os operadores serão sobrecarregados para a classe derivada new/deletee as funções definidas e alocadas na classe base serão usadas para alocar e liberar memória.

Agora considere uma hierarquia de classes polimórfica, em que cada classe sobrecarrega os operadores new/delete. Agora, deixe a instância da classe derivada ser excluída usando o operador deleteatravés de um ponteiro para a classe base. Se o destruidor da classe base for virtual, o padrão garantirá que o destruidor dessa classe derivada seja chamado. Nesse caso operator delete(), a chamada de função definida para esta classe derivada também é garantida . Assim, a função é operator delete()realmente virtual.

2.2.5 Forma alternativa da função delete () do operador


Em uma classe (especialmente quando a herança é usada), às vezes é conveniente usar uma forma alternativa da função para liberar memória:

void operator delete(void* p, std::size_t size);
void operator delete[](void* p, std::size_t size);

O parâmetro sizedefine o tamanho do elemento (mesmo na versão da matriz). Este formulário permite usar funções diferentes para alocar e liberar memória, dependendo da classe derivada específica.

3. Operadores de usuários novos / excluídos


O C ++ pode oferecer suporte a formulários personalizados do operador, do newseguinte formato:

new(/*  */) T(/*   */)
new(/*  */) T[/*   */]

Para que esses formulários sejam suportados, é necessário determinar as funções apropriadas para alocar e liberar memória:

void* operator new(std::size_t size, /* .  */);
void* operator new[](std::size_t size, /* .  */);
void operator delete(void* p, /* .  */);
void operator delete[](void* p, /* .  */);

A lista de parâmetros adicionais das funções de alocação de memória não deve estar vazia e não deve consistir em uma void*ou const std::nothrow_t&, ou seja, sua assinatura não deve coincidir com uma das padrão. Lista de parâmetros adicionais operator new()e operator delete()deve corresponder. Os argumentos passados ​​para o operador newdevem corresponder a parâmetros adicionais das funções de alocação de memória. Uma função personalizada operator delete()também pode estar no formulário com um parâmetro de tamanho opcional.

Essas funções podem ser definidas no espaço para nome global ou em uma classe, mas não em um espaço para nome que não seja global. Se eles são definidos no espaço de nomes global, eles não substituem, mas sobrecarregam, as funções padrão de alocação e liberação de memória; portanto, seu uso é previsível e seguro, e as funções padrão estão sempre disponíveis. Se eles são definidos na classe, ocultam os formulários padrão, mas o acesso aos formulários padrão pode ser obtido usando o operador ::, isso é descrito em detalhes na seção 2.2.

Os formulários de operador definidos pelo usuário newsão chamados newde posicionamento definido pelo usuário new. Eles não devem ser confundidos com o operador de posicionamento padrão (não alocado) newdescrito na seção 1.2.

O formulário do operador correspondente deletenão existe. Existem newduas maneiras de excluir um objeto criado usando um operador definido pelo usuário . Se uma função definida pelo usuário operator new()delegar uma operação de alocação de memória para funções de alocação de memória padrão, um operador padrão poderá ser usado delete. Caso contrário, você precisará chamar explicitamente o destruidor e, em seguida, a função definida pelo usuário operator delete(). O compilador chama a função definida pelo usuário em operator delete()apenas um caso: quando o newconstrutor lança uma exceção durante a operação do operador definido pelo usuário .

Aqui está um exemplo (no escopo global).

void* operator new(std::size_t size, int a, const char* b)
{
    std::cout << "new " << a << " + " << b << "\n";
    return ::operator new(size);
}
void operator delete(void* p, int a, const char* b)
{
    std::cout << "delete " << a << " + " << b << "\n";
    ::operator delete(p);
}

class X {/* ... */};
X* p = new(42, "meow") X(); // : new 42 + meow
delete p; //   ::operator delete()

4. Definição de funções de alocação de memória


Nestes exemplos, funções de usuário operator new()e operator delete()operação delegada correspondentes às funções padrão. Às vezes, essa opção é útil, mas o principal objetivo da sobrecarga new/deleteé criar um novo mecanismo para alocar / liberar memória. A tarefa não é simples e, antes de realizá-la, é preciso pensar cuidadosamente em tudo. Scott Meyers [Meyers1] discute possíveis motivos para tomar tal decisão (é claro, o principal é a eficiência). Ele também discute os principais problemas técnicos associados à implementação correta de funções definidas pelo usuário para alocar e liberar memória (usando a funçãoset_new_handler(), sincronização multithread, alinhamento). Guntheroth fornece um exemplo da implementação de funções de alocação e desalocação de memória definidas pelo usuário relativamente simples. Antes de criar sua própria versão, você deve procurar soluções prontas, como exemplo, você pode trazer a biblioteca Pool do projeto Boost.

5. Classes de alocadores de contêineres padrão


Como mencionado acima, os contêineres padrão usam classes especiais de alocador para alocar e liberar memória. Essas classes são parâmetros de modelo de contêineres e o usuário pode definir sua versão dessa classe. Os motivos para tal solução são aproximadamente os mesmos que para sobrecarregar os operadores new/delete. [Guntheroth] descreve como criar essas classes.

Bibliografia


[Guntheroth]
Gunteroth, Kurt. Otimização de programas em C ++. Métodos comprovados para aumentar a produtividade. do inglês - SPb.: Alpha-book LLC, 2017.
[Meyers1]
Meyers, Scott. Uso efetivo de C ++. 55 maneiras seguras de melhorar a estrutura e o código dos seus programas. do inglês - M .: DMK Press, 2014.

All Articles