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
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/delete
utilisées pour créer et supprimer un objet et un tableau du type sont T
les 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é, delete
tout 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érateurdelete
selon 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érateurdelete
appliquer 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 new
permettent 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 new
placement sans allocation new
. Un argument ptr
est 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 new
n'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 delete
pour 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/delete
utilisent 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 new
ne 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/delete
consiste Ă 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 new
ne peut pas ĂȘtre dĂ©finie dans l'espace de noms global. AprĂšs une telle dĂ©finition, les opĂ©rateurs correspondants new/delete
les 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/delete
pour 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 globalvoid* 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 new
est 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/delete
dans l'espace de noms global est fortement déconseillée.2.2. Surcharge de classe
La surcharge des opérateurs new/delete
dans 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/delete
peuvent ĂȘ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, X
nous 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 new
avec 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 X
dans 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/delete
et 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 delete
via 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 size
dĂ©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 new
forme 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 new
doivent 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 new
sont appelés new
emplacements définis par l'utilisateur new
. Ils ne doivent pas ĂȘtre confondus avec l'opĂ©rateur de placement standard (sans allocation) new
décrit à la section 1.2.Le formulaire opérateur correspondant delete
n'existe pas. Il new
existe 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 new
constructeur 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();
delete p;
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/delete
est 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.