172 votes

Comment lancer des exceptions std::avec des messages variables ?

Voici un exemple de ce que je fais souvent lorsque je veux ajouter des informations à une exception :

std::stringstream errMsg;
errMsg << "Could not load config file '" << configfile << "'";
throw std::exception(errMsg.str().c_str());

Y a-t-il une façon plus agréable de le faire ?

6voto

evilrix Points 132

Peut-être ça ?

throw std::runtime_error(
    (std::ostringstream()
        << "Could not load config file '"
        << configfile
        << "'"
    ).str()
);

Il crée un ostringstream temporaire, appelle les opérateurs << si nécessaire et ensuite vous enveloppez le tout entre des crochets ronds et appelez la fonction .str() sur le résultat évalué (qui est un ostringstream) pour passer une std::string temporaire au constructeur de runtime_error.

Note : l'ostringstream et la chaîne sont des temporaires de valeur r et sortent donc de la portée après la fin de cette ligne. Le constructeur de votre objet d'exception DOIT prendre la chaîne en entrée en utilisant la sémantique copy ou (mieux) move.

Supplémentaire : Je ne considère pas nécessairement cette approche comme une "meilleure pratique", mais elle fonctionne et peut être utilisée à la rigueur. L'un des plus gros problèmes est que cette méthode nécessite des allocations de tas et donc l'opérateur << peut lancer. Vous ne voulez probablement pas que cela se produise ; cependant, si vous vous retrouvez dans cet état, vous avez probablement d'autres problèmes à régler !

2voto

Il y a deux points à répondre en ce qui concerne ce que vous voulez :

1.

Le premier point est que la manière la plus agréable est de créer des types spéciaux (classes) pour les exceptions personnalisées et de passer les paramètres comme champs des classes.

Quelque chose comme le suivant :

class BaseFor_Exceptions : public std::exception {
protected:
    BaseFor_Exceptions();
};

class Exception1 : public BaseFor_Exceptions {
public:
    Exception1(uint32_t value1);
private:
    uint32_t value1;
};

throw Exception1(0);

Le deuxième point est que vous effectuez des allocations de mémoire lors de la préparation de l'objet d'exception parce que vous essayez de passer une valeur de taille variable (nom de fichier).

Il est possible (lors du changement d'objets de std::string et std::stringstream) que l'exception std::bad_alloc soit lancée au cours du processus, de sorte que si vous ne préparez pas ou ne lancez pas (*) votre exception, vous perdrez l'information et l'état.

Dans un programme bien conçu, il est facile d'éviter l'allocation de mémoire lors de la préparation ou du traitement d'une exception. Il suffit de le faire :

  • soit garantir que la valeur est toujours vivante lors du traitement de l'exception et transmettre une sorte de lien vers la valeur dans le cadre de l'exception - soit une référence, soit une sorte de pointeur (le plus souvent intelligent),
  • ou obtenir la valeur lors du traitement de l'exception en utilisant l'information sur le type d'exception ou/et les valeurs à taille fixe ; par exemple,

    } catch (const ConfigurationLoadError & ex) {

    std::cerr
        << “Some message 1 ”
        << serviceLocator1.SomeGetMethod1().Get_ConfigurationFileName();

    } catch (const SomeException & ex) {

    std::cerr
        << “Some message 2 ”
        << serviceLocator1.SomeGetMethod2().GetEventDetailsString(ex.Get_Value1());

    }

Bien entendu, vous avez toujours la possibilité d'accepter les limites de taille de la mémoire tampon et d'utiliser une mémoire tampon pré-allouée.

Veuillez également noter que les types (classes) utilisés pour les exceptions ne sont pas autorisés à lancer des exceptions à partir de leurs constructeurs de copie car, si l'exception initiale est tentée d'être attrapée par valeur, un appel du constructeur de copie est possible (dans le cas où il n'est pas élidé par le compilateur) et cette exception supplémentaire interrompra le traitement de l'exception initiale avant que celle-ci ne soit attrapée, ce qui entraîne l'appel de std::terminate. Depuis C++11, les compilateurs sont autorisés à éliminer la copie dans certains cas lors de la capture, mais l'élision n'est pas toujours judicieuse et, si elle l'est, ce n'est qu'une permission et non une obligation (cf. https://en.cppreference.com/w/cpp/language/copy_elision pour plus de détails ; avant C++11, les normes du langage ne réglementaient pas la question).

De plus, vous devriez éviter que les exceptions (nous les appellerons les supplémentaires) soient lancées par les constructeurs et les constructeurs de déplacement de vos types (classes) utilisés pour les exceptions (nous les appellerons les initiales) car les constructeurs et les constructeurs de déplacement pourraient être appelés lors du lancement d'objets des types en tant qu'exceptions initiales, puis le lancement d'une exception supplémentaire empêcherait la création d'un objet d'exception initiale, et l'initiale serait simplement perdue. De même qu'une exception supplémentaire provenant d'un constructeur de copie, lors du lancement d'une exception initiale, provoquerait la même chose.

0voto

bpeikes Points 93

J'ai rencontré un problème similaire, dans la mesure où la création de messages d'erreur personnalisés pour mes exceptions personnalisées rend le code laid. Voici ma solution :

class MyRunTimeException: public std::runtime_error
{
public:
      MyRunTimeException(const std::string &filename):std::runtime_error(GetMessage(filename)) {}
private:
      static std::string GetMessage(const std::string &filename)
     {
           // Do your message formatting here. 
           // The benefit of returning std::string, is that the compiler will make sure the buffer is good for the length of the constructor call
           // You can use a local std::ostringstream here, and return os.str()
           // Without worrying that the memory is out of scope. It'll get copied
           // You also can create multiple GetMessage functions that take all sorts of objects and add multiple constructors for your exception
     }
}

Cela sépare la logique de création des messages. J'avais d'abord pensé à surcharger what(), mais alors vous devez capturer votre message quelque part. std::runtime_error a déjà un tampon interne.

0voto

Chaque fois que j'ai besoin d'un message personnalisé à lancer dans une exception, je construis une chaîne de style C avec snprintf() et le passer au constructeur de l'exception.

if (problem_occurred) {
    char buffer[200];
    snprintf(buffer, 200, "Could not load config file %s", configfile);
    string error_mesg(buffer);
    throw std::runtime_error(error_mesg);
}

Je ne suis pas sûr que la chaîne supplémentaire string error_mesg(buffer) est nécessaire. J'estime que le buffer est sur la mémoire de la pile, et si le collecteur d'exception continue à fonctionner, alors permettre au collecteur de garder une référence à une chaîne C allouée à la pile est problématique. Au lieu de cela, le passage d'un string à l'exception invoquera la copie par valeur, et l'option buffer sera copié à fond.

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