68 votes

Comment devrais-je écrire des opérateurs de création et de suppression personnalisés conformes à la norme ISO C ++?

Comment dois-je écrire ISO C++ standard conforme personnalisée new et delete opérateurs?

C'est dans le prolongement de la Surcharge new et delete dans le très éclairant C++ FAQ, la surcharge d'Opérateur, et son suivi, Pourquoi devrait-on remplacer nouveau par défaut et supprimer des opérateurs?

Section 1: la Rédaction d'une norme-conforme new de l'opérateur

Section 2: la Rédaction d'une norme-conforme delete de l'opérateur

(Note: Ceci est destiné à être une entrée à Débordement de Pile du C++ FAQ. Si vous voulez une critique de l'idée de fournir une FAQ dans ce formulaire, puis de la poster sur meta qui a commencé tout cela serait l'endroit pour le faire. Les réponses à cette question sont surveillés en C++ salon, où la FAQ idée a commencé à en premier lieu, de sorte que votre réponse est très probablement le faire lire par ceux qui sont venus avec l'idée.)
Remarque: La réponse est basée sur les enseignements de Scott Meyers Plus Efficace C++ et le Standard ISO C++.

40voto

Alok Save Points 115848

Partie I

Cette entrée de la FAQ C++ explique pourquoi on peut avoir envie de surcharge new et delete opérateurs pour sa propre classe. Cette FAQ tente d'expliquer comment on le fait dans un standard conforme.

La mise en œuvre d'une coutume new de l'opérateur

La norme C++ (§18.4.1.1) définit operator new comme:

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

Le C++ standard spécifie la sémantique que des versions personnalisées de ces opérateurs ont à obéir au §3.7.3 et §18.4.1

Résumons les exigences.

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

La fonction d'allocation vise à répartir le montant demandé de stockage. Si elle est réussie, elle doit renvoyer l'adresse de début d'un bloc de stockage dont la longueur en octets doivent être au moins aussi grand que la taille demandée...

La norme impose en outre:

...Le pointeur retourné doit être correctement alignés de sorte qu'il peut être converti en un pointeur de chaque type d'objet, puis utilisé pour accéder à l'objet ou le tableau dans le stockage alloué (jusqu'à ce que le stockage est explicitement libéré par un appel à une fonction de libération). Même si la taille de l'espace requis est égal à zéro, la requête peut échouer. Si la demande aboutit, la valeur retournée est une non-valeur de pointeur null (4.10) p0 différente de celle précédemment renvoyée valeur p1, à moins que la valeur de p1 est ensuite transmis à un opérateur delete.

Cela nous donne en outre à des exigences importantes:

Exigence n ° 2: La fonction d'allocation mémoire nous utilisons (habituellement malloc() ou certains autres allocateur personnalisé) doit retourner une correctement alignés pointeur vers la mémoire allouée, qui peut être converti en un pointeur d'un type d'objet et utilisé pour accéder à l'objet.

Exigence n ° 3: Notre propre opérateur new doit retourner une légitime pointeur de même lors de zéro octets sont demandés.

L'un des manifestes les exigences qui peuvent même être déduite à partir de new prototype:

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

Mais! Il n'y a plus que ce que rencontre l'oeil: Si vous regardez de plus près à l' new de l'opérateur de la documentation (citation de la norme suit plus bas), il indique:

Si set_new_handler a été utilisé pour définir une new_handler fonction, ce new_handler fonction est appelée par le standard de la définition par défaut de operator new si il ne peut pas allouer de la demande de stockage par son propre.

Pour comprendre comment notre coutume new besoins à l'appui de cette exigence, il faut comprendre:

Quel est le new_handler et set_new_handler?

new_handler est une définition de type d'un pointeur vers une fonction qui prend et ne renvoie rien, et set_new_handler est une fonction qui prend et retourne un new_handler.

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

Un moment opportun pour un exemple de code pour mettre les choses au clair:

#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 (le plus probable) sera impossible d'allouer de l'espace pour 100 000 000 d'entiers, et la fonction outOfMemHandler() sera appelée, et le programme s'arrêtera après l'émission d'un message d'erreur.

Il est important de noter ici que, lors de l' operator new est incapable de remplir une demande de mémoire, il appelle l' new-handler fonction à plusieurs reprises jusqu'à ce qu'il peut trouver suffisamment de mémoire ou il n'y a plus de nouveaux gestionnaires. Dans l'exemple ci-dessus, à moins que nous appelons std::abort(), outOfMemHandler() serait appelé à plusieurs reprises. Par conséquent, le gestionnaire doit s'assurer que la prochaine allocation réussit, ou inscrire un autre gestionnaire, ou n'enregistrent pas de gestionnaire, ou pas de retour (c'est à dire la fin du programme). Si il n'y a pas de nouvelles de gestionnaire et de l'allocation échoue, l'opérateur va lancer une exception.

Suite 1


23voto

Alok Save Points 115848

Partie II

... suite

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

Libérer de la mémoire: Cela peut permet la prochaine allocation de mémoire tentative à l'intérieur de nouvel opérateur de boucle pour réussir. Une façon de mettre en œuvre ce qui est d'allouer un bloc de mémoire au démarrage du programme, puis relâchez-la pour l'utiliser dans le programme la première fois le nouveau gestionnaire est appelé.

Installez un autre nouveau-chien: Si le nouveau gestionnaire peut pas faire plus de mémoire disponible, et de là est un autre nouveau-gestionnaire peut, alors le courant de la nouvelle-gestionnaire peut installer les autres nouveau-chien à sa place (en appelant set_new_handler). La prochaine fois opérateur de nouveaux appels à la nouvelle fonction de gestionnaire, elle permettra d'obtenir la plus récente.

(Une variation sur ce thème est pour un nouveau gestionnaire pour modifier son propre comportement, de sorte que la prochaine fois qu'il est invoqué, il fait quelque chose de différent. Une façon d'y parvenir est d'avoir la nouvelle-gestionnaire de modifier statique, de l'espace de noms spécifique, ou de données mondiale qui affecte le nouveau gestionnaire du comportement.)

Désinstaller la nouvelle-gestionnaire: Cela se fait en passant un pointeur null pour set_new_handler. En l'absence de nouveau gestionnaire d'installé, operator new va lever une exception (convertibles) std::bad_alloc) lors de l'allocation de mémoire est un échec.

Lever une exception convertibles std::bad_alloc. Ces exceptions ne sont pas pris en operator new, mais qui va se propager vers le site d'origine de la demande de mémoire.

Pas de retour: En appelant abort ou exit.

Pour mettre en œuvre une classe spécifique new_handler nous avons de fournir une classe avec ses propres versions de l' set_new_handler et operator new. La classe set_new_handler permet aux clients de spécifier la nouvelle-gestionnaire pour la classe (exactement comme la norme set_new_handlerpermet aux clients de spécifier la nouvelle-gestionnaire). La classe operator new s'assure que les classe spécifique de la nouvelle-gestionnaire est utilisé à la place de la global new-gestionnaire de la mémoire pour les objets de classe est attribué.


Maintenant que nous comprenons new_handler & set_new_handler mieux nous sommes en mesure de modifier l' Exigence n ° 4 convenablement:

Exigence N ° 4 (Amélioré):
Notre operator new devrait essayer d'allouer de la mémoire plus d'une fois, l'appel de la nouvelle fonction de traitement après chaque échec. L'hypothèse ici est que la nouvelle fonction de gestion pourraient être en mesure de faire quelque chose pour libérer de la mémoire. Seulement lorsque le pointeur de la nouvelle fonction de gestion est - null t operator new de lever une exception.

Comme promis, la référence de la Norme:
Section 3.7.4.1.3:

Une fonction d'allocation qui ne parvient pas à allouer du stockage peut invoquer la actuellement installés new_handler(18.4.2.2), le cas échéant. [Note: Un programme fourni par l'allocation de fonction peut obtenir l'adresse de la actuellement installés new_handler à l'aide de l' set_new_handler de la fonction (18.4.2.3).] Si une fonction d'allocation déclaré avec un vide spécification d'exception (15.4), throw(), ne parvient pas à allouer du stockage, il doit retourner un pointeur null. Toute autre fonction d'allocation qui ne parvient pas à allouer du stockage doit seulement indiquer l'échec par la jet-ing une exception de la classe std::bad_alloc (18.4.2.1) ou d'une classe dérivée de l' std::bad_alloc.

Armé avec le #4 exigences, nous allons tenter 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

    }

}

Poursuite 2

19voto

Alok Save Points 115848

La partie III

... suite

Notez que nous ne pouvons pas obtenir le nouveau gestionnaire de pointeur de fonction directement, nous devons faire appel set_new_handler pour savoir ce que c'est. C'est rudimentaire mais efficace, au moins pour single-threaded code. Dans un environnement multithread, probablement une sorte de verrou à manipuler le (global) des structures de données sous-tend la nouvelle fonction de traitement sera nécessaire. (Plus d'citation/détails sont les bienvenus sur ce.)

Aussi, nous avons une boucle infinie et le seul moyen de sortir de la boucle est de la mémoire pour être correctement alloué, ou pour la nouvelle fonction de gestion des pour en faire l'une des choses que nous avons déduit avant. À moins que l' new_handler t une de ces choses, cette boucle à l'intérieur d' new de l'opérateur aura jamais de fin.

Une mise en garde: Notez que la norme (§3.7.4.1.3, cité ci-dessus) ne disent pas explicitement que les surchargées new opérateur doit mettre en œuvre une boucle infinie, mais il ne fait que dire que c'est le comportement par défaut. Si ce détail est ouvert à l'interprétation, mais la plupart des compilateurs (GCC et Microsoft Visual C++) faire mettre en œuvre cette fonctionnalité boucle (vous pouvez compiler les exemples de code fournis plus tôt). Aussi, depuis une C++ authory comme Scott Meyers suggère cette approche, il est assez 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 cette FAQ, explique, une raison commune pour l'écriture d'un personnalisé du gestionnaire de mémoire est d'optimiser l'allocation pour les objets d'une classe spécifique, pas pour une classe ou d'une de ses classes dérivées, ce qui signifie essentiellement que notre nouvel opérateur pour la classe de Base est généralement à l'écoute pour des objets de taille sizeof(Base) -rien de plus et rien de plus petites.

Dans l'exemple ci-dessus, en raison de l'héritage de la classe dérivée Derived hérite le nouvel exploitant de la classe de Base. Cela rend l'opérateur appelant de nouveau 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 pour gérer cette situation consiste à détourner ces appels demandant la "mauvaise" la quantité de mémoire à la norme de l'opérateur 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 aussi incoprporates notre exigence n ° 3. C'est parce que tous autoportant les objets ont une taille non nulle en C++, donc, sizeof(Base) ne peut jamais être égal à zéro, donc si la taille est égale à zéro, la demande sera transmise à l' ::operator new, et il est gauranteed qu'il va la traiter en conforme à la norme.

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

17voto

Alok Save Points 115848

La mise en œuvre d'une coutume opérateur delete

La Norme C++ (§18.4.1.1) bibliothèque définit operator delete comme:

void operator delete(void*) throw();

Nous répétons l'exercice de collecte des exigences pour l'écriture de notre coutume operator delete:

Exigence n ° 1: Il est de retour void et son premier paramètre est void*. Une coutume delete operator peut avoir plus d'un seul paramètre, mais bien nous avons juste besoin d'un paramètre à passer le pointeur pointant vers la mémoire allouée.

Citation de la Norme C++:

La Section §3.7.3.2.2:

"Chaque fonction de libération est de retour void et son premier paramètre est vide*. Une fonction de libération peut avoir plus d'un paramètre....."

Exigence n ° 2: Il doit garantir qu'il est plus sûr de supprimer un pointeur null dans le passé comme argument.

Citation de C++ Standard: La Section §3.7.3.2.3:

La valeur du premier argument fourni à l'un de la libération de la mémoire de fonctions de la bibliothèque standard, peut être une valeur de pointeur null; si donc, l'appel à la fonction de libération n'a pas d'effet. Sinon, la valeur fournie à l' operator delete(void*) dans la bibliothèque standard doit être l'une des valeurs retournées par un appel précédent de operator new(size_t) ou operator new(size_t, const std::nothrow_t&) dans la bibliothèque standard, et la valeur fournie à l' operator delete[](void*) dans la bibliothèque standard doit être l'une des valeurs retournées par un appel précédent de operator new[](size_t) ou operator new[](size_t, const std::nothrow_t&) dans la bibliothèque standard.

Exigence n ° 3: Si le pointeur passé n'est pas null, puis l' delete operator devrait libérer la dynamique de la mémoire allouée et attribuée au pointeur.

Citation de C++ Standard: La Section §3.7.3.2.4:

Si l'argument donné à une fonction de libération de la bibliothèque standard est un pointeur qui n'est pas la valeur de pointeur null (4.10), la fonction de libération est de libérer le stockage référencé par le pointeur, le rendu invalide tous les pointeurs se référant à une partie de la désallocation de stockage.

Exigence n ° 4: Aussi, depuis notre spécifiques à la classe de nouvel opérateur transfère les demandes de la "mauvaise" de la taille d' ::operator new, Nous DEVONS avancer "à tort" de la taille de la suppression des demandes d' ::operator delete.

Donc, sur la base des exigences, nous avons résumé ci-dessus est ici d'un standard conforme pseudo-code pour une coutume delete operator:

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X