35 votes

Que se passe-t-il si la fonction "throw" ne parvient pas à allouer de la mémoire à l'objet d'exception ?

Extrait de la norme C++11 (15.1.p4) :

La mémoire de l'objet d'exception est allouée d'une manière non spécifiée, à l'exception de ce qui est indiqué dans la section 3 7 4 1. sauf comme indiqué dans la section 3.7.4.1.

Et si l'allocation échoue, est-ce qu'elle lancera std::bad_alloc à la place ? Appelez std::terminate ? Non spécifié ?

11voto

C.M. Points 777

(fournir ma propre réponse... Je vais attendre quelques jours et s'il n'y a pas de problèmes avec elle -- je la marquerai comme acceptée)

J'ai passé un certain temps à enquêter sur ce sujet et voici ce que j'ai découvert :

  • La norme C++ ne précise pas ce qui va se passer dans ce cas.
  • Clang et GCC semblent utiliser C++ Itanium ABI

L'ABI d'Itanimum suggère d'utiliser le tas pour les exceptions :

Le stockage est nécessaire pour les exceptions qui sont lancées. Ce stockage doit persister pendant que la pile est déroulée, puisqu'elle sera utilisée par le gestionnaire, et doit être thread-safe. Le stockage des objets d'exception sera donc normalement alloué dans le tas

...

La mémoire sera allouée par le __cxa_allocate_exception routine de la bibliothèque d'exécution.

Donc, oui... lancer une exception impliquera probablement le verrouillage des mutex et la recherche d'un bloc de mémoire libre :-(

Il mentionne également ceci :

Si __cxa_allocate_exception ne peut pas allouer un objet d'exception sous ces contraintes, elle appelle terminate()

Ouaip... dans GCC et Clang "throw myX() ;" peut tuer votre application et vous ne pouvez rien y faire (peut-être écrire votre propre __cxa_allocate_exception peut aider -- mais il ne sera certainement pas portable)

C'est encore mieux :

3.4.1 Allocation de l'objet d'exception

La mémoire d'un objet d'exception sera allouée par la fonction __cxa_allocate_exception de la bibliothèque d'exécution, avec les exigences générales décrites à la section 2.4.2. Si l'allocation normale échoue, elle tentera d'allouer l'un des tampons d'urgence, décrits dans la Section 3.3.1, avec les contraintes suivantes :

  • La taille de l'objet d'exception, y compris les en-têtes, est inférieure à 1KB.
  • Le thread actuel ne détient pas déjà quatre tampons.
  • Il y a moins de 16 autres threads détenant des tampons, ou ce fil attendra que l'un des autres libère ses tampons. avant d'en acquérir un.

Oui, votre programme peut tout simplement se bloquer ! Il est vrai que les chances que cela se produise sont faibles -- il faudrait que vous épuisiez la mémoire et que vos threads utilisent les 16 tampons d'urgence et entrent en attente d'un autre thread qui devrait générer une exception. Mais si vous faites des choses avec std::current_exception (comme enchaîner les exceptions et les passer entre les threads) - ce n'est pas si improbable.

Conclusion :

Il s'agit d'une lacune de la norme C++ : vous ne pouvez pas écrire des programmes fiables à 100 % (qui utilisent des exceptions). L'exemple type est un serveur qui accepte les connexions des clients et exécute les tâches soumises. L'approche évidente pour gérer les problèmes serait de lancer une exception, qui déroulerait tout et fermerait la connexion -- tous les autres clients ne seraient pas affectés et le serveur continuerait à fonctionner (même dans des conditions de mémoire faible). Hélas, un tel serveur est impossible à écrire en C++.

Vous pouvez prétendre que les systèmes modernes (c'est-à-dire Linux) tueront un tel serveur avant que nous n'atteignions cette situation de toute façon. Mais (1) ce n'est pas un argument ; (2) le gestionnaire de mémoire peut être configuré pour surcommettre ; (3) le tueur OOM ne sera pas déclenché pour une application 32 bits fonctionnant sur un matériel 64 bits avec suffisamment de mémoire (ou si l'application limite artificiellement l'allocation de mémoire).

Personnellement, je suis assez énervé par cette découverte. Pendant de nombreuses années, j'ai prétendu que mon code gérait gracieusement le hors mémoire. Il s'avère que j'ai menti à mes clients :-( Autant commencer à intercepter l'allocation de mémoire, appeler std::terminate et traiter toutes les fonctions connexes comme noexcept -- Cela me facilitera certainement la vie (en matière de codage). Pas étonnant qu'ils utilisent encore Ada pour programmer les fusées.

3voto

Igor Tandetnik Points 13562

[intro.conformité]/2 Bien que la présente Norme internationale n'énonce que des exigences relatives aux implémentations C++, ces exigences sont souvent plus faciles à comprendre si elles sont formulées comme des exigences relatives aux programmes, aux parties de programmes ou à l'exécution des programmes. De telles exigences ont la signification suivante :

(2.1) - Si un programme ne contient aucune violation des règles de la présente norme internationale, une mise en œuvre conforme doit, dans les limites de ses ressources accepter et exécuter correctement ce programme.

C'est moi qui souligne. Fondamentalement, la norme envisage l'échec de l'allocation de mémoire dynamique (et prescrit un comportement dans ce cas), mais pas tout autre type de mémoire ; et ne prescrit en aucune façon ce que l'implémentation devrait faire lorsque ses limites de ressources sont atteintes.

Un autre exemple est de se retrouver à court de pile à cause d'une récursion trop profonde. La norme ne précise nulle part à quelle profondeur une récursion est autorisée. Le débordement de pile qui en résulte est l'implémentation qui exerce son droit à l'échec "dans les limites des ressources".

2voto

Ivan Points 3403

La réponse actuelle décrit déjà ce que fait GCC. J'ai vérifié le comportement de MSVC - il alloue les exceptions sur la pile, donc l'allocation ne dépend pas du tas. Cela rend le débordement de pile possible (l'objet exception peut être grand), mais la gestion du débordement de pile n'est pas couverte par le standard C++.

J'ai utilisé ce court programme pour examiner ce qui se passe pendant le lancement d'une exception :

#include <iostream>

class A {
public:
    A() { std::cout << "A::A() at " << static_cast<void *>(this) << std::endl; }
    A(const A &) { std::cout << "A::A(const A &) at " << static_cast<void *>(this) << std::endl; }
    A(A &&) { std::cout << "A::A(A &&) at " << static_cast<void *>(this) << std::endl; }
    ~A() { std::cout << "A::~A() at " << static_cast<void *>(this) << std::endl; }
    A &operator=(const A &) = delete;
    A &operator=(A &&) = delete;
};

int main()
{
    try {
        try {
            try {
                A a;
                throw a;
            } catch (const A &ex) {
                throw;
            }
        } catch (const A &ex) {
            throw;
        }
    } catch (const A &ex) {
    }
}

Lorsque l'on construit avec GCC, la sortie montre clairement que l'exception lancée est allouée loin de la pile :

A::A() at 0x22cad7
A::A(A &&) at 0x600020510
A::~A() at 0x22cad7
A::~A() at 0x600020510

Lorsque l'on construit avec MSVC, la sortie montre que l'exception est allouée à proximité sur la pile :

A::A() at 000000000018F4E4
A::A(A &&) at 000000000018F624
A::~A() at 000000000018F4E4
A::~A() at 000000000018F624

Un examen supplémentaire avec le débogueur montre que les gestionnaires de capture et les destructeurs sont exécutés au sommet de la pile, de sorte que la consommation de la pile augmente avec chaque bloc de capture, en commençant par le premier lancer et jusqu'à ce que std::uncaught_exceptions() devient 0.

Un tel comportement signifie qu'une gestion correcte des sorties de mémoire exige que vous prouviez qu'il y a suffisamment d'espace dans la pile pour que le programme puisse exécuter les gestionnaires d'exceptions et tous les destructeurs en cours de route.

Pour prouver la même chose avec GCC, il semble que vous devrez prouver qu'il n'y a pas plus de quatre exceptions imbriquées et que les exceptions ont une taille inférieure à 1KiB (ceci inclut l'en-tête). De plus, si un thread a plus de quatre exceptions imbriquées, vous devez également prouver qu'il n'y a pas de blocage causé par une allocation de tampon d'urgence.

0voto

Oliv Points 7148

En réalité, il est spécifié que si l'allocation de l'objet d'exception échoue, bad_alloc devrait être lancée et l'implémentation pourrait également appeler le nouveau gestionnaire.

Voici ce qui est spécifié dans la section de la norme c++ (§3.7.4.1) où vous vous trouvez [basic.stc.dynamic.allocation] :

Une fonction d'allocation qui ne parvient pas à allouer du stockage peut invoquer la fonction new-handler actuellement installée (21.6.3.3), le cas échéant. [ Note : Une fonction d'allocation fournie par un programme peut obtenir l'adresse de la fonction new-handler actuellement installée nouveau_handler en utilisant la fonction std::get_new_handler (21.6.3.4). - note de fin ] Si une allocation qui a une spécification d'exception non létale (18.4) ne parvient pas à allouer de l'espace de stockage, elle doit renvoyer un pointeur nul. nul. Toute autre fonction d'allocation qui ne parvient pas à allouer de l'espace de stockage doit indiquer l'échec uniquement en lançant une exception (18.1) d'un type qui correspondrait à un gestionnaire. exception (18.1) d'un type qui correspondrait à un gestionnaire (18.3) de type std::bad_alloc (21.6.3.1).

Ensuite, ceci a été rappelé dans [except.terminate]

Dans certaines situations, la gestion des exceptions doit être abandonnée au profit de techniques de gestion des erreurs moins subtiles. [ Note : Ces situations sont : - (1.1) lorsque le mécanisme de traitement des exceptions, après avoir terminé l'initialisation de l'objet d'exception mais avant l'activation d'un gestionnaire de l'exception (18.1)*.

Ainsi, l'ABI de l'itanium ne suit pas la spécification standard de c++, puisqu'il peut bloquer ou appeler terminate si le programme ne parvient pas à allouer de la mémoire pour l'objet d'exception.

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