web-dev-qa-db-fra.com

Comment écrire des opérateurs new et delete personnalisés conformes à la norme ISO C ++?

Comment écrire les opérateurs personnalisés new et delete conformes à la norme ISO C++?

Ceci est dans la suite de Surcharge de nouveau et de suppression dans la FAQ C++ immensément éclairante, Surcharge d'opérateur , et son suivi, Pourquoi devrait-on remplacer par défaut new et supprimer les opérateurs?

Section 1: Ecrire un opérateur new conforme au standard

Section 2: Ecrire un opérateur delete conforme au standard

(Remarque: Ceci est censé être une entrée de FAQ C++ de Stack Overflow . Si vous voulez critiquer l'idée de fournir un FAQ sous cette forme , alors la publication sur la méta qui a commencé tout cela serait l'endroit pour faire cela. Les réponses à cette question sont surveillées dans le chatroom C++ , où le FAQ l'idée a commencé en premier lieu, donc votre réponse est très susceptible d'être lue par ceux qui ont eu l'idée.)
Remarque: La réponse est basée sur les enseignements tirés du C++ plus efficace de Scott Meyers et de la norme ISO C++.

64
Alok Save

Partie I

Ce C++ FAQ entrée expliqué pourquoi on pourrait vouloir surcharger les opérateurs new et delete pour sa propre classe. Ce présent FAQ essaie d'expliquer comment on le fait d'une manière conforme à la norme.

Implémentation d'un opérateur new personnalisé

Le standard C++ (§18.4.1.1) définit operator new Comme:

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

Le standard C++ spécifie la sémantique que les versions personnalisées de ces opérateurs doivent obéir aux §3.7.3 et §18.4.1

Résumons les exigences.

Condition n ° 1: Il doit allouer dynamiquement au moins size octets de mémoire et renvoyer un pointeur vers la mémoire allouée. Citation de la norme C++, section 3.7.4.1.3:

La fonction d'allocation tente d'allouer la quantité de stockage demandée. S'il réussit, il renvoie l'adresse de début d'un bloc de stockage dont la longueur en octets doit être au moins aussi grande que la taille demandée ...

La norme impose en outre:

... Le pointeur renvoyé doit être correctement aligné afin qu'il puisse être converti en un pointeur de n'importe quel type d'objet complet, puis utilisé pour accéder à l'objet ou au tableau dans le stockage alloué (jusqu'à ce que le stockage soit explicitement désalloué par un appel à un fonction de désallocation). Même si la taille de l'espace demandé est égale à zéro, la demande peut échouer. Si la demande aboutit, la valeur renvoyée doit être une valeur de pointeur non nulle (4.10) p0 différente de toute valeur p1 précédemment renvoyée, à moins que cette valeur p1 n'ait été ultérieurement transmise à un opérateur delete.

Cela nous donne d'autres exigences importantes:

Condition n ° 2: La fonction d'allocation de mémoire que nous utilisons (généralement malloc() ou un autre allocateur personnalisé) doit renvoyer un convenablement aligné pointeur vers la mémoire allouée, qui peut être convertie en un pointeur d'un type d'objet complet et utilisé pour accéder à l'objet.

Condition n ° 3: Notre opérateur personnalisé new doit renvoyer un pointeur légitime même lorsque zéro octet est demandé.

L'une des exigences évidentes qui peuvent même être déduites du prototype new est:

Condition n ° 4: Si new ne peut pas allouer de mémoire dynamique de la taille demandée, alors il doit lever une exception de type std::bad_alloc.

Mais! Il y a plus que ce que vous voyez: si vous regardez de plus près l'opérateur newdocumentation (la citation de la norme suit plus bas), il déclare:

Si set_new_handler a été utilisé pour définir un new_handler, cette fonction new_handler est appelée par la définition standard par défaut de operator new si elle ne peut pas allouer la mémoire demandée par elle-même.

Pour comprendre comment notre new personnalisé doit prendre en charge cette exigence, nous devons comprendre:

Qu'est-ce que new_handler Et set_new_handler?

new_handler Est un typedef pour un pointeur vers une fonction qui ne prend et ne renvoie rien, et set_new_handler Est une fonction qui prend et renvoie un new_handler.

Le paramètre de set_new_handler Est un pointeur vers la fonction que l'opérateur new devrait appeler s'il ne peut pas allouer la mémoire demandée. Sa valeur de retour est un pointeur vers la fonction de gestionnaire précédemment enregistrée, ou null s'il n'y avait pas de gestionnaire précédent.

Un moment opportun pour un exemple de code pour clarifier les choses:

#include <iostream>
#include <cstdlib>

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

Dans l'exemple ci-dessus, operator new (Très probablement) ne pourra pas allouer d'espace pour 100 000 000 d'entiers, et la fonction outOfMemHandler() sera appelée et le programme abandonnera après émission un message d'erreur .

Il est important de noter ici que lorsque operator new Est incapable de répondre à une demande de mémoire, il appelle la fonction new-handler À plusieurs reprises jusqu'à ce qu'il peut trouver suffisamment de mémoire ou là n'est plus de nouveaux gestionnaires. Dans l'exemple ci-dessus, à moins que nous n'appelions std::abort(), outOfMemHandler() serait appelé à plusieurs reprises . Par conséquent, le gestionnaire doit soit s'assurer que l'allocation suivante réussit, soit enregistrer un autre gestionnaire, soit enregistrer aucun gestionnaire, ou ne pas retourner (c'est-à-dire terminer le programme). S'il n'y a pas de nouveau gestionnaire et que l'allocation échoue, l'opérateur lèvera une exception.

Suite 1


32
Alok Save

Deuxieme PARTIE

... suite

Étant donné le comportement de operator new De l'exemple, un new_handler bien conçu doit effectuer l'une des opérations suivantes:

Rendre plus de mémoire disponible: Cela peut permettre à la prochaine tentative d'allocation de mémoire dans la boucle de l'opérateur new de réussir. Une façon de l'implémenter est d'allouer un gros bloc de mémoire au démarrage du programme, puis de le libérer pour une utilisation dans le programme la première fois que le nouveau gestionnaire est appelé.

Installez un nouveau gestionnaire différent: Si le nouveau gestionnaire actuel ne peut pas rendre plus de mémoire disponible, et qu'il y a un autre nouveau gestionnaire qui peut , alors le new-handler courant peut installer l'autre new-handler à sa place (en appelant set_new_handler). La prochaine fois que l'opérateur new appelle la fonction new-handler, il obtiendra la dernière installée.

(Une variante de ce thème est pour un nouveau gestionnaire de modifier son propre comportement, donc la prochaine fois qu'il est appelé, il fait quelque chose de différent. Une façon d'y parvenir est de demander au nouveau gestionnaire de modifier statique, spécifique à l'espace de noms ou données globales qui affectent le comportement du nouveau gestionnaire.)

Désinstallez le nouveau gestionnaire: Ceci est fait en passant un pointeur nul à set_new_handler. Sans nouveau gestionnaire installé, operator new Lèvera une exception ((convertible en) std::bad_alloc) Lorsque l'allocation de mémoire échoue.

Lance une exception convertible en std::bad_alloc. De telles exceptions ne seront pas interceptées par operator new, Mais se propageront au site à l'origine de la demande de mémoire.

Ne pas retourner: En appelant abort ou exit.

Pour implémenter un new_handler Spécifique à une classe, nous devons fournir une classe avec ses propres versions de set_new_handler Et operator new. La classe set_new_handler Permet aux clients de spécifier le nouveau gestionnaire de la classe (exactement comme le standard set_new_handler Permet aux clients de spécifier le nouveau gestionnaire global). La classe operator new Garantit que le new-handler spécifique à la classe est utilisé à la place du new-handler global lorsque la mémoire pour les objets de classe est allouée.


Maintenant que nous comprenons mieux new_handler & set_new_handler, Nous sommes en mesure de modifier la Exigence # 4 convenablement comme:

Exigence n ° 4 (améliorée):
Notre operator new Devrait essayer d'allouer de la mémoire plus d'une fois, en appelant la fonction de gestion des nouvelles après chaque échec. L'hypothèse ici est que la nouvelle fonction de gestion pourrait être capable de faire quelque chose pour libérer de la mémoire. Ce n'est que lorsque le pointeur vers la nouvelle fonction de gestion est null que operator new Lève une exception.

Comme promis, la citation de la norme:
Section 3.7.4.1.3:

Une fonction d'allocation qui ne parvient pas à allouer de l'espace de stockage peut appeler le new_handler (18.4.2.2) Actuellement installé, le cas échéant. [Remarque: Une fonction d'allocation fournie par le programme peut obtenir l'adresse du new_handler Actuellement installé en utilisant la fonction set_new_handler (18.4.2.3).] Si une fonction d'allocation déclarée avec un vide spécification d'exception (15.4), throw(), ne parvient pas à allouer le stockage, il doit renvoyer un pointeur nul. Toute autre fonction d'allocation qui ne parvient pas à allouer le stockage doit uniquement indiquer l'échec en lançant une exception de classe std::bad_alloc (18.4.2.1) Ou une classe dérivée de std::bad_alloc.

Armés des exigences # 4 , essayons le pseudo code pour notre 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

    }

}

Suite 2

19
Alok Save

Partie III

... suite

Notez que nous ne pouvons pas obtenir directement le nouveau pointeur de la fonction de gestionnaire, nous devons appeler set_new_handler Pour savoir ce que c'est. C'est grossier mais efficace, du moins pour le code monothread. Dans un environnement multithread, une sorte de verrou pour manipuler en toute sécurité les structures de données (globales) derrière la nouvelle fonction de gestion sera probablement nécessaire. ( Plus de citations/détails sont les bienvenus à ce sujet.)

De plus, nous avons une boucle infinie et le seul moyen de sortir de la boucle est que la mémoire soit allouée avec succès, ou que la fonction de nouvelle gestion fasse l'une des choses que nous avons déduites auparavant. À moins que new_handler Ne fasse une de ces choses, cette boucle à l'intérieur de l'opérateur new ne se terminera jamais.

Une mise en garde: Notez que le standard (§3.7.4.1.3, Cité ci-dessus) ne dit pas explicitement que l'opérateur new surchargé doit implémenter une boucle infinie, mais cela dit simplement que tel est le comportement par défaut. Ce détail est donc ouvert à interprétation, mais la plupart des compilateurs ( GCC et Microsoft Visual C++ ) implémentent cette fonctionnalité de boucle ( vous pouvez compiler les exemples de code fournis précédemment). De plus, comme une authentification C++ telle que Scott Meyers suggère cette approche, elle est suffisamment raisonnable.

Scénarios spéciaux

Considérons le scénario suivant.

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

Comme l'explique this FAQ, une raison courante d'écrire un gestionnaire de mémoire personnalisé est d'optimiser l'allocation pour les objets d'une classe spécifique , pas pour une classe ou l'une de ses classes dérivées, ce qui signifie fondamentalement que notre opérateur nouveau pour la classe Base est généralement réglé pour des objets de taille sizeof(Base) - rien de plus grand ni de plus petit.

Dans l'exemple ci-dessus, en raison de l'héritage, la classe dérivée Derived hérite du nouvel opérateur de la classe de base. Cela rend l'opérateur d'appel new dans une classe de base pour allouer de la mémoire pour un objet d'une classe dérivée possible. La meilleure façon pour notre operator new De gérer cette situation est de dévier de tels appels demandant la "mauvaise" quantité de mémoire vers l'opérateur standard new, comme ceci:

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

Notez que la vérification de la taille intègre également notre exigence # 3 . En effet, tous les objets autonomes ont une taille non nulle en C++, donc sizeof(Base) ne peut jamais être zéro, donc si la taille est nulle, la requête sera transmise à ::operator new, Et c'est garanti qu'il le traitera de manière conforme aux normes.

Citation: Du créateur de C++ lui-même, le Dr Bjarne Stroustrup.

16
Alok Save

Implémentation d'un opérateur de suppression personnalisé

La bibliothèque C++ Standard (§18.4.1.1) Définit operator delete Comme suit:

void operator delete(void*) throw();

Répétons l'exercice de collecte des prérequis pour l'écriture de notre operator delete Personnalisé:

Condition n ° 1: Il retournera void et son premier paramètre sera void*. Un delete operator Personnalisé peut également avoir plus d'un paramètre, mais nous avons juste besoin d'un paramètre pour passer le pointeur pointant vers la mémoire allouée.

Citation de la norme C++:

Section §3.7.3.2.2:

"Chaque fonction de désallocation doit retourner void et son premier paramètre doit être void *. Une fonction de désallocation peut avoir plus d'un paramètre ....."

Condition n ° 2: Cela doit garantir qu'il est sûr de supprimer un pointeur nul passé en argument.

Citation de la norme C++: Section §3.7.3.2.3:

La valeur du premier argument fourni à l'une des fonctions de désallocation fournies dans la bibliothèque standard peut être une valeur de pointeur nulle; si tel est le cas, l'appel à la fonction de désallocation n'a aucun effet. Sinon, la valeur fournie à operator delete(void*) dans la bibliothèque standard doit être l'une des valeurs renvoyées par une précédente invocation de operator new(size_t) ou operator new(size_t, const std::nothrow_t&) dans la bibliothèque standard, et la valeur fournie à operator delete[](void*) dans la bibliothèque standard doit être l'une des valeurs renvoyées par un précédent appel de operator new[](size_t) ou operator new[](size_t, const std::nothrow_t&) dans la bibliothèque standard.

Condition # 3: Si le pointeur passé n'est pas null, alors le delete operator Devrait désallouer la mémoire dynamique allouée et affectée au pointeur.

Citation de la norme C++: Section §3.7.3.2.4:

Si l'argument donné à une fonction de désallocation dans la bibliothèque standard est un pointeur qui n'est pas la valeur de pointeur nulle (4.10), la fonction de désallocation doit désallouer le stockage référencé par le pointeur, rendant invalides tous les pointeurs faisant référence à n'importe quelle partie du stockage désalloué.

Condition n ° 4: De plus, puisque notre opérateur spécifique à la classe transmet les requêtes de la "mauvaise" taille à ::operator new, Nous DEVONS transmettre les demandes de suppression "mal dimensionnées" à ::operator delete.

Donc, sur la base des exigences que nous avons résumées ci-dessus, voici un pseudo-code conforme standard pour un delete operator Personnalisé:

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;
        }
};
12
Alok Save