¿Cómo lanzar std :: excepciones con mensajes variables?

121

Este es un ejemplo de lo que hago a menudo cuando quiero agregar información a una excepción:

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

¿Existe una forma mejor de hacerlo?

Ben
fuente
10
Me pregunto cómo lograste trabajar de esta manera: std∷exceptionno tienen un constructor con char*arg.
Hi-Angel
2
Me pregunto lo mismo. ¿Quizás es una extensión de MS no estándar para c ++? ¿O quizás algo nuevo en C ++ 14? La documentación actual dice que el constructor std :: exception no acepta argumentos.
Chris Warth
1
Sí, pero std::stringtiene un constructor implícito que toma un const char*...
Brice M. Dempsey
6
@Chris Warth Parece ser parte de la implementación detrás de escena de std::exceptionlas clases secundarias de MS , y es utilizado por sus versiones de std::runtime_errory std::logic_error. Aparte de los definidos por el estándar, la versión de MSVS <exception>también incluye dos constructores más, uno tomando (const char * const &)y el otro tomando (const char * const &, int). Se utilizan para establecer una variable privada const char * _Mywhat; si _Mywhat != nullptr, por what()defecto lo devuelve. El código que se basa en él probablemente no sea portátil.
Justin Time - Reincorpora a Monica

Respuestas:

49

Aquí está mi solución:

#include <stdexcept>
#include <sstream>

class Formatter
{
public:
    Formatter() {}
    ~Formatter() {}

    template <typename Type>
    Formatter & operator << (const Type & value)
    {
        stream_ << value;
        return *this;
    }

    std::string str() const         { return stream_.str(); }
    operator std::string () const   { return stream_.str(); }

    enum ConvertToString 
    {
        to_str
    };
    std::string operator >> (ConvertToString) { return stream_.str(); }

private:
    std::stringstream stream_;

    Formatter(const Formatter &);
    Formatter & operator = (Formatter &);
};

Ejemplo:

throw std::runtime_error(Formatter() << foo << 13 << ", bar" << myData);   // implicitly cast to std::string
throw std::runtime_error(Formatter() << foo << 13 << ", bar" << myData >> Formatter::to_str);    // explicitly cast to std::string
Torsten
fuente
1
Dios mío, he estado buscando cómo hacer algo como esto. Pero probablemente cambiará el operador >> a una función explícita para evitar la sobrecarga (sobrecarga del operador)
Roman Plášil
3
¿Cuál es la diferencia entre esto y un std :: stringstream? Parece contener una secuencia de cadenas, pero (hasta donde yo sé) no tiene ninguna funcionalidad adicional.
matts1
2
Generalmente, no es una forma 100% segura. Los métodos std :: stringstream pueden generar una excepción. El problema se describe bastante bien aquí: boost.org/community/error_handling.html
Arthur P. Golubev
1
@ ArthurP.Golubev Pero en este caso, una instancia de Formatter () también crea una instancia de un stringstream detrás de escena, que nuevamente, podría generar una excepción. Entonces, ¿cuál es la diferencia?
Zuzu Corneliu
La única funcionalidad agregada es el truco ConvertToString y la conversión explícita a cadena, lo cual es bueno de todos modos. ;)
Zuzu Corneliu
178

Las excepciones estándar se pueden construir a partir de std::string:

#include <stdexcept>

char const * configfile = "hardcode.cfg";
std::string const anotherfile = get_file();

throw std::runtime_error(std::string("Failed: ") + configfile);
throw std::runtime_error("Error: " + anotherfile);

Tenga en cuenta que la clase base std::exceptionpuede no estar construido de este modo; tienes que usar una de las clases derivadas concretas.

Kerrek SB
fuente
27

Hay diferentes excepciones como runtime_error, range_error, overflow_error, logic_error, etc .. Es necesario para pasar la cadena en su constructor, y puede concatenar lo que quiera a su mensaje. Eso es solo una operación de cadena.

std::string errorMessage = std::string("Error: on file ")+fileName;
throw std::runtime_error(errorMessage);

También puedes usar boost::formatasí:

throw std::runtime_error(boost::format("Error processing file %1") % fileName);
Neel Basu
fuente
La versión boost :: format anterior no se compilará sin una conversión explícita, es decir: runtime_error ((boost :: format ("Text% 1"% 2) .str ())). C ++ 20 introduce un formato std :: que proporcionará una funcionalidad similar.
Digicrat
17

La siguiente clase puede resultar muy útil:

struct Error : std::exception
{
    char text[1000];

    Error(char const* fmt, ...) __attribute__((format(printf,2,3))) {
        va_list ap;
        va_start(ap, fmt);
        vsnprintf(text, sizeof text, fmt, ap);
        va_end(ap);
    }

    char const* what() const throw() { return text; }
};

Ejemplo de uso:

throw Error("Could not load config file '%s'", configfile.c_str());
Maxim Egorushkin
fuente
4
En mi opinión, mala práctica, ¿por qué usar algo como esto cuando ya hay una biblioteca estándar que está diseñada para la optimización?
Jean-Marie Comets
3
throw std::runtime_error(sprintf("Could not load config file '%s'", configfile.c_str()))
Jean-Marie Comets
4
throw std::runtime_error("Could not load config file " + configfile);(convirtiendo uno u otro argumento a std::stringsi es necesario).
Mike Seymour
9
@MikeSeymour Sí, pero eso se pone más feo si necesitas poner cadenas en el medio y formatear números con cierta precisión, etc. Es difícil superar una buena cadena de formato antiguo en términos de claridad.
Maxim Egorushkin
2
@MikeSeymour Puedo estar de acuerdo en que el código que publiqué puede estar adelantado a su tiempo. El tipo portátil seguro printfy los amigos son inminentes en C ++ 11. El búfer de tamaño fijo es tanto una bendición como una maldición: no falla en situaciones de bajos recursos, pero puede truncar el mensaje. Considero que truncar un mensaje de error es una mejor opción que fallar. Además, la conveniencia de las cadenas de formato ha sido probada en muchos idiomas diferentes. Pero tienes razón, es en gran parte una cuestión de gustos.
Maxim Egorushkin
11

Utilice un operador literal de cadena si C ++ 14 ( operator ""s)

using namespace std::string_literals;

throw std::exception("Could not load config file '"s + configfile + "'"s);

o defina el suyo si está en C ++ 11. Por ejemplo

std::string operator ""_s(const char * str, std::size_t len) {
    return std::string(str, str + len);
}

Tu declaración de lanzamiento se verá así

throw std::exception("Could not load config file '"_s + configfile + "'"_s);

que se ve bonito y limpio.

Shreevardhan
fuente
2
Recibí este error c ++ \ 7.3.0 \ bits \ exception.h | 63 | nota: no hay función coincidente para la llamada a 'std :: exception :: exception (std :: __ cxx11 :: basic_string <char>)
HaseeB Mir
El comportamiento descrito por @Shreevardhan no está definido en la biblioteca estándar, aunque MSVC ++ lo compilará.
jochen
0

Una forma realmente mejor sería crear una clase (o clases) para las excepciones.

Algo como:

class ConfigurationError : public std::exception {
public:
    ConfigurationError();
};

class ConfigurationLoadError : public ConfigurationError {
public:
    ConfigurationLoadError(std::string & filename);
};

La razón es que las excepciones son mucho más preferibles que simplemente transferir una cadena. Al proporcionar diferentes clases para los errores, les da a los desarrolladores la oportunidad de manejar un error en particular de la manera correspondiente (no solo mostrar un mensaje de error). Las personas que detectan tu excepción pueden ser tan específicas como necesiten si usas una jerarquía.

a) Es posible que deba saber la razón específica

} catch (const ConfigurationLoadError & ex) {
// ...
} catch (const ConfigurationError & ex) {

a) otro no quiere saber detalles

} catch (const std::exception & ex) {

Puede encontrar algo de inspiración sobre este tema en https://books.google.ru/books?id=6tjfmnKhT24C Capítulo 9

Además, también puede proporcionar un mensaje personalizado, pero tenga cuidado, no es seguro redactar un mensaje de una std::stringu std::stringstreamotra forma que pueda causar una excepción .

En general, no hay diferencia si asigna memoria (trabaja con cadenas en C ++) en el constructor de la excepción o justo antes de lanzarla; la std::bad_allocexcepción se puede lanzar antes de la que realmente desea.

Entonces, un búfer asignado en la pila (como en la respuesta de Maxim) es una forma más segura.

Se explica muy bien en http://www.boost.org/community/error_handling.html

Entonces, la forma más agradable sería un tipo específico de excepción y evitar componer la cadena formateada (al menos al lanzar).

Arthur P. Golubev
fuente
0

Me encontré con un problema similar, ya que la creación de mensajes de error personalizados para mis excepciones personalizadas crea un código desagradable. Esta fue mi solución:

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

Esto separa la lógica para crear los mensajes. Originalmente había pensado en anular qué (), pero luego tienes que capturar tu mensaje en alguna parte. std :: runtime_error ya tiene un búfer interno.

bpeikes
fuente
0

¿Tal vez esto?

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

Crea un flujo de avestruz temporal, llama a los operadores << según sea necesario y luego lo envuelve entre paréntesis y llama a la función .str () en el resultado evaluado (que es un flujo de avestruz) para pasar un std :: cadena temporal al constructor de runtime_error.

Nota: ostringstream y la cadena son temporales de valor r y, por lo tanto, quedan fuera del alcance después de que finaliza esta línea. El constructor de su objeto de excepción DEBE tomar la cadena de entrada usando la semántica de copiar o (mejor) mover.

Adicional: No considero necesariamente este enfoque como una "mejor práctica", pero funciona y se puede utilizar en un apuro. Uno de los mayores problemas es que este método requiere asignaciones de montón y, por lo tanto, el operador << puede lanzar. Probablemente no quieras que eso suceda; sin embargo, si llega a ese estado, probablemente tenga muchos más problemas de los que preocuparse.

evilrix
fuente