Surcharge en C ++. Partie III. Surcharge des instructions new / delete


Nous continuons la sĂ©rie "C ++, creuser en profondeur." Le but de cette sĂ©rie est d'en dire le plus possible sur les diffĂ©rentes caractĂ©ristiques du langage, peut-ĂȘtre assez particuliĂšres. Cet article concerne la surcharge des opĂ©rateurs new/delete. Il s'agit du troisiĂšme article de la sĂ©rie, le premier dĂ©diĂ© aux fonctions et modĂšles de surcharge, situĂ© ici , le deuxiĂšme dĂ©diĂ© aux opĂ©rateurs de surcharge, situĂ© ici . Cet article conclut une sĂ©rie de trois articles sur la surcharge en C ++.


Table des matiĂšres


Table des matiĂšres
  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. Formes standard des opérateurs nouveaux / supprimés


C ++ prend en charge plusieurs options d'opĂ©rateur new/delete. Ils peuvent ĂȘtre divisĂ©s en standard de base, standard supplĂ©mentaire et personnalisĂ©. Cette section et la section 2 traitent des formulaires standard; les formulaires personnalisĂ©s seront abordĂ©s dans la section 3.

1.1. Formulaires standard de base


Les principales formes standard d'opérateurs new/deleteutilisées pour créer et supprimer un objet et un tableau du type sont Tles suivantes:

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

Leur travail peut ĂȘtre dĂ©crit comme suit. Lorsque l'opĂ©rateur est appelĂ© new, la mĂ©moire est d'abord allouĂ©e Ă  l'objet. Si la sĂ©lection rĂ©ussit, le constructeur est appelĂ©. Si le constructeur lĂšve une exception, la mĂ©moire allouĂ©e est libĂ©rĂ©e. Lorsque l'opĂ©rateur est appelĂ©, deletetout se passe dans l'ordre inverse: d'abord, le destructeur est appelĂ©, puis la mĂ©moire est libĂ©rĂ©e. Le destructeur ne doit pas lever d'exceptions.

Lorsque l'opérateurnew[]utilisée pour créer un tableau d'objets, la mémoire est d'abord allouée à l'ensemble du tableau. Si la sélection réussit, le constructeur par défaut (ou un autre constructeur, s'il y a un initialiseur) est appelé pour chaque élément du tableau à partir de zéro. Si un constructeur lÚve une exception, alors pour tous les éléments créés du tableau, le destructeur est appelé dans l'ordre inverse de l'appel du constructeur, la mémoire allouée est libérée. Pour supprimer un tableau, vous devez appeler l'opérateur delete[]et pour tous les éléments du tableau, le destructeur est appelé dans l'ordre inverse du constructeur, puis la mémoire allouée est libérée.

Attention! Il est nĂ©cessaire d'appeler la bonne forme de l'opĂ©rateurdeleteselon si un seul objet ou un tableau est supprimĂ©. Cette rĂšgle doit ĂȘtre strictement respectĂ©e, sinon vous pouvez obtenir un comportement indĂ©fini, c'est-Ă -dire que tout peut arriver: fuites de mĂ©moire, crash, etc. Voir [Meyers1] pour plus de dĂ©tails.

Dans la description ci-dessus, une clarification est nĂ©cessaire. Pour les soi-disant types triviaux (types intĂ©grĂ©s, structures de style C), le constructeur par dĂ©faut ne peut pas ĂȘtre appelĂ© et le destructeur ne fait en aucun cas quoi que ce soit.

Les fonctions d'allocation de mĂ©moire standard, lorsqu'il n'est pas possible de satisfaire la demande, lĂšvent une exception de type std::bad_alloc. Mais cette exception peut ĂȘtre interceptĂ©e, pour cela, vous devez installer un intercepteur global Ă  l'aide d'un appel de fonction set_new_handler(), pour plus de dĂ©tails, voir [Meyers1].

Toute forme d'opérateurdeleteappliquer en toute sécurité au pointeur nul.

Lors de la crĂ©ation d'un tableau avec un opĂ©rateur, la new[]taille peut ĂȘtre dĂ©finie sur zĂ©ro.

Les deux formes de l'opérateur newpermettent d'utiliser des initialiseurs dans les accolades.

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

1.2. Formulaires standard supplémentaires


Lors de la connexion du fichier d'en-tĂȘte <new>, 4 formulaires d'opĂ©rateur standard supplĂ©mentaires sont disponibles new:

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

Les deux premiers d'entre eux sont appelĂ©s newplacement sans allocation new. Un argument ptrest un pointeur vers une rĂ©gion de mĂ©moire suffisamment grande pour contenir une instance ou un tableau. De plus, la zone de mĂ©moire doit avoir un alignement appropriĂ©. Cette version de l'opĂ©rateur newn'alloue pas de mĂ©moire, elle fournit uniquement un appel au constructeur. Ainsi, cette option vous permet de sĂ©parer les phases d'allocation de mĂ©moire et d'initialisation des objets. Cette fonctionnalitĂ© est activement utilisĂ©e dans les conteneurs standard. L'opĂ©rateur deletepour les objets ainsi crĂ©Ă©s ne peut bien entendu pas ĂȘtre appelĂ©. Pour supprimer un objet, vous devez appeler directement le destructeur, puis libĂ©rer la mĂ©moire d'une maniĂšre qui dĂ©pend de la mĂ©thode d'allocation de mĂ©moire.

Les deux autres options sont appelées l'opérateur ne lançant pas d'exceptions new(nothrow new) et diffÚrent en ce que s'il est impossible de satisfaire la demande, elles renvoient nullptr, mais ne lÚvent pas d'exception de type std::bad_alloc. La suppression d'un objet se produit à l'aide de l'opérateur principal delete. Ces options sont considérées comme obsolÚtes et non recommandées.

1.3. Allocation de mémoire et fonctions libres


Les opérateurs standard new/deleteutilisent les fonctions d'allocation et de désallocation suivantes:

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);

Ces fonctions sont définies dans l'espace de noms global. Les fonctions d'allocation de mémoire pour les instructions host newne font rien et retournent simplement ptr.

C ++ 17 a pris en charge d'autres formes d'allocation de mémoire et de désallocation, indiquant l'alignement. En voici quelques uns:

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

Ces formulaires ne sont pas directement accessibles à l'utilisateur, ils sont utilisés par le compilateur pour des objets dont les exigences d'alignement sont supérieures __STDCPP_DEFAULT_NEW_ALIGNMENT__, donc le principal problÚme est que l'utilisateur ne les cache pas accidentellement (voir section 2.2.1). Rappelons qu'en C ++ 11, il est devenu possible de définir explicitement l'alignement des types d'utilisateurs.

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

2. Surcharger les formulaires standard des opérateurs nouveaux / supprimer


La surcharge des formes standard d'opĂ©rateurs new/deleteconsiste Ă  dĂ©finir des fonctions dĂ©finies par l'utilisateur pour allouer et libĂ©rer de la mĂ©moire dont les signatures coĂŻncident avec celles standard. Ces fonctions peuvent ĂȘtre dĂ©finies dans l'espace de noms global ou dans une classe, mais pas dans un espace de noms autre que global. La fonction d'allocation de mĂ©moire pour une instruction hĂŽte standard newne peut pas ĂȘtre dĂ©finie dans l'espace de noms global. AprĂšs une telle dĂ©finition, les opĂ©rateurs correspondants new/deleteles utiliseront, pas les opĂ©rateurs standard.

2.1. Surcharge dans l'espace de noms global


Supposons, par exemple, dans un module dans un espace de noms global que des fonctions définies par l'utilisateur soient définies:

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

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

Dans ce cas, il y aura en fait un remplacement (remplacement) des fonctions standard pour allouer et libérer de la mémoire pour tous les appels d'opérateur new/deletepour toutes les classes (y compris les classes standard) dans le module entier. Cela peut conduire au chaos complet. Notez que le mécanisme de substitution décrit est un mécanisme spécial implémenté uniquement pour ce cas, et non un mécanisme C ++ général. Dans ce cas, lors de l'implémentation des fonctions utilisateur d'allocation et de libération de mémoire, il devient impossible d'appeler les fonctions standard correspondantes, elles sont complÚtement masquées (l'opérateur ::n'aide pas), et lorsque vous essayez de les appeler, un appel récursif à la fonction utilisateur se produit.

Fonction définie par l'espace de noms global

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

Il remplacera également le standard, mais il y aura moins de problÚmes potentiels, car l'opérateur qui ne lance pas d'exceptions newest rarement utilisé. Mais le formulaire standard n'est pas non plus disponible.

La mĂȘme situation avec des fonctions pour les tableaux.

La surcharge des instructions new/deletedans l'espace de noms global est fortement déconseillée.

2.2. Surcharge de classe


La surcharge des opérateurs new/deletedans une classe est dépourvue des inconvénients décrits ci-dessus. La surcharge n'est efficace que lors de la création et de la suppression d'instances de la classe correspondante, quel que soit le contexte d'appel des opérateurs new/delete. Lorsque vous implémentez des fonctions définies par l'utilisateur pour allouer et libérer de la mémoire à l'aide de l'opérateur, ::vous pouvez accéder aux fonctions standard correspondantes. Prenons un exemple.

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);
    }
};

Dans cet exemple, le suivi est simplement ajouté aux opérations standard. Maintenant, en termes new X()et new X[N]utiliser ces fonctions pour allouer et libérer de la mémoire.

Ces fonctions sont formellement statiques et peuvent ĂȘtre dĂ©clarĂ©es comme static. Mais pour l'essentiel, ce sont des instances, avec l'appel de fonction operator new(), la crĂ©ation de l'instance commence et l'appel de fonction operator delete()termine sa suppression. Ces fonctions ne sont jamais appelĂ©es pour d'autres tĂąches. De plus, comme cela sera montrĂ© ci-dessous, la fonction operator delete()est essentiellement virtuelle. Il est donc plus correct de les dĂ©clarer sans static.

2.2.1. AccÚs aux formulaires standard des opérateurs nouveaux / supprimés


Les opĂ©rateurs new/deletepeuvent ĂȘtre utilisĂ©s avec un opĂ©rateur de rĂ©solution d'Ă©tendue supplĂ©mentaire, par exemple ::new(p) X(). Dans ce cas, la fonction operator new()dĂ©finie dans la classe sera ignorĂ©e et la norme correspondante sera utilisĂ©e. De la mĂȘme maniĂšre, vous pouvez utiliser l'opĂ©rateur delete.

2.2.2. Masquage d'autres formes d'opérateurs nouveaux / supprimés


Si maintenant, pour la classe, Xnous essayons d'utiliser des exceptions de lancement ou de non-lancement new, nous obtenons une erreur. Le fait est que la fonction operator new(std::size_t size)masquera d'autres formes operator new(). Le problĂšme peut ĂȘtre rĂ©solu de deux maniĂšres. Dans le premier, vous devez ajouter les options appropriĂ©es Ă  la classe (ces options devraient simplement dĂ©lĂ©guer le fonctionnement de la fonction standard). Dans le second, vous devez utiliser un opĂ©rateur newavec un opĂ©rateur de rĂ©solution de portĂ©e, par exemple ::new(p) X().

2.2.3. Conteneurs standard


Si nous essayons de placer les instances Xdans un conteneur standard, par exemple std::vector<X>, nous verrons que nos fonctions ne sont pas utilisées pour allouer et libérer de la mémoire. Le fait est que tous les conteneurs standard ont leur propre mécanisme d'allocation et de libération de mémoire (une classe d'allocateur spéciale, qui est un paramÚtre de modÚle du conteneur), et ils utilisent un opérateur de placement pour initialiser les éléments new.

2.2.4. HĂ©ritage


Les fonctions d'allocation et de libération de mémoire sont héritées. Si ces fonctions sont définies dans la classe de base, mais pas dans la classe dérivée, les opérateurs seront surchargés pour la classe dérivée new/deleteet les fonctions définies et allouées dans la classe de base seront utilisées pour allouer et libérer de la mémoire.

ConsidĂ©rons maintenant une hiĂ©rarchie de classes polymorphe, oĂč chaque classe surcharge les opĂ©rateurs new/delete. Maintenant, laissez l'instance de la classe dĂ©rivĂ©e supprimĂ©e Ă  l'aide de l'opĂ©rateur deletevia un pointeur vers la classe de base. Si le destructeur de la classe de base est virtuel, la norme garantit que le destructeur de cette classe dĂ©rivĂ©e est appelĂ©. Dans ce cas operator delete(), l' appel de fonction dĂ©fini pour cette classe dĂ©rivĂ©e est Ă©galement garanti . Ainsi, la fonction est en operator delete()rĂ©alitĂ© virtuelle.

2.2.5. Forme alternative de la fonction delete () de l'opérateur


Dans une classe (surtout quand l'héritage est utilisé), il est parfois pratique d'utiliser une forme alternative de la fonction pour libérer de la mémoire:

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

Le paramĂštre sizedĂ©finit la taille de l'Ă©lĂ©ment (mĂȘme dans la version du tableau). Ce formulaire vous permet d'utiliser diffĂ©rentes fonctions pour allouer et libĂ©rer de la mĂ©moire, selon la classe dĂ©rivĂ©e spĂ©cifique.

3. Opérateurs utilisateurs nouveaux / supprimés


C ++ peut prendre en charge les formulaires d'opérateur personnalisés de la newforme suivante:

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

Pour que ces formulaires soient pris en charge, il est nécessaire de déterminer les fonctions appropriées pour l'allocation et la libération de mémoire:

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

La liste des paramĂštres supplĂ©mentaires des fonctions d'allocation de mĂ©moire ne doit pas ĂȘtre vide et ne doit pas consister en une void*ou const std::nothrow_t&, qui est, leur signature ne doit pas coĂŻncider avec l' un des standards. Liste des paramĂštres supplĂ©mentaires dans operator new()et operator delete()doit correspondre. Les arguments transmis Ă  l'opĂ©rateur newdoivent correspondre Ă  des paramĂštres supplĂ©mentaires des fonctions d'allocation de mĂ©moire. Une fonction personnalisĂ©e operator delete()peut Ă©galement ĂȘtre dans le formulaire avec un paramĂštre de taille facultatif.

Ces fonctions peuvent ĂȘtre dĂ©finies dans l'espace de noms global ou dans une classe, mais pas dans un espace de noms autre que global. S'ils sont dĂ©finis dans l'espace de noms global, ils ne remplacent pas, mais surchargent, les fonctions standard d'allocation et de libĂ©ration de mĂ©moire, leur utilisation est donc prĂ©visible et sĂ»re, et les fonctions standard sont toujours disponibles. S'ils sont dĂ©finis dans la classe, ils masquent les formulaires standard, mais l'accĂšs aux formulaires standard peut ĂȘtre obtenu en utilisant l'opĂ©rateur ::, cela est dĂ©crit en dĂ©tail dans la section 2.2.

Les formulaires d' opĂ©rateur dĂ©finis par l' utilisateur newsont appelĂ©s newemplacements dĂ©finis par l'utilisateur new. Ils ne doivent pas ĂȘtre confondus avec l'opĂ©rateur de placement standard (sans allocation) newdĂ©crit Ă  la section 1.2.

Le formulaire opĂ©rateur correspondant deleten'existe pas. Il newexiste deux façons de supprimer un objet crĂ©Ă© Ă  l'aide d'un opĂ©rateur dĂ©fini par l'utilisateur . Si une fonction dĂ©finie par l'utilisateur operator new()dĂ©lĂšgue une opĂ©ration d'allocation de mĂ©moire Ă  des fonctions d'allocation de mĂ©moire standard, un opĂ©rateur standard peut ĂȘtre utilisĂ© delete. Sinon, vous devrez appeler explicitement le destructeur, puis la fonction dĂ©finie par l'utilisateur operator delete(). Le compilateur appelle la fonction dĂ©finie par l'utilisateur dans operator delete()un seul cas: lorsque le newconstructeur lĂšve une exception pendant le fonctionnement de l'opĂ©rateur dĂ©fini par l'utilisateur .

Voici un exemple (de portée globale).

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. Définition des fonctions d'allocation de mémoire


Dans ces exemples, les fonctions utilisateur operator new()et les operator delete()opĂ©rations dĂ©lĂ©guĂ©es correspondant aux fonctions standard. Parfois, cette option est utile, mais le but principal de la surcharge new/deleteest de crĂ©er un nouveau mĂ©canisme d'allocation / libĂ©ration de mĂ©moire. La tĂąche n'est pas simple et avant d'entreprendre, il faut bien rĂ©flĂ©chir Ă  tout. Scott Meyers [Meyers1] discute des motifs possibles pour prendre une telle dĂ©cision (bien sĂ»r, le principal est l'efficacitĂ©). Il discute Ă©galement des principaux problĂšmes techniques liĂ©s Ă  la mise en Ɠuvre correcte des fonctions dĂ©finies par l'utilisateur pour l'allocation et la libĂ©ration de mĂ©moire (en utilisant la fonctionset_new_handler(), synchronisation multi-thread, alignement). Guntheroth fournit un exemple de mise en Ɠuvre de fonctions d'allocation de mĂ©moire et de dĂ©sallocation dĂ©finies par l'utilisateur relativement simples. Avant de crĂ©er votre propre version, vous devez rechercher des solutions prĂȘtes Ă  l'emploi, par exemple, vous pouvez apporter la bibliothĂšque Pool du projet Boost.

5. Classes d'allocateurs de conteneurs standard


Comme mentionnĂ© ci-dessus, les conteneurs standard utilisent des classes d'allocation spĂ©ciales pour allouer et libĂ©rer de la mĂ©moire. Ces classes sont des paramĂštres de modĂšle de conteneurs et l'utilisateur peut dĂ©finir sa version d'une telle classe. Les motifs d'une telle solution sont Ă  peu prĂšs les mĂȘmes que pour les opĂ©rateurs de surcharge new/delete. [Guntheroth] dĂ©crit comment crĂ©er de telles classes.

Bibliographie


[Guntheroth]
Gunteroth, Kurt. Optimisation des programmes en C ++. Méthodes éprouvées pour augmenter la productivité.: Per. de l'anglais - SPb.: Alpha-book LLC, 2017.
[Meyers1]
Meyers, Scott. Utilisation efficace de C ++. 55 façons sûres d'améliorer la structure et le code de vos programmes.: Per. de l'anglais - M .: DMK Press, 2014.

All Articles