如何抛出带有可变消息的 std: : 异常?

这是我在向异常添加信息时经常做的一个例子:

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

还有更好的办法吗?

223667 次浏览

标准异常可以从 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);

注意,基类 std::exception可以这样构造 没有; 您必须使用具体的派生类之一。

下面的课程可能会很方便:

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

用法例子:

throw Error("Could not load config file '%s'", configfile.c_str());

也有不同的例外,如 runtime_errorrange_erroroverflow_errorlogic_error等。您需要将字符串传递给它的构造函数,并且您可以将任何内容连接到您的消息。那只是一个字符串操作。

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

你也可以这样使用 boost::format:

throw std::runtime_error(boost::format("Error processing file %1") % fileName);

我的解决办法是:

#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 &);
};

例如:

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

关于你想要什么,有两点需要回答:

1.

第一点是,更好的方法是为自定义异常创建特殊类型(类) ,并将参数作为类的字段传递。

大致如下:

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

第二点是在准备异常对象时执行内存分配,因为试图传递一个可变大小的值(文件名)。

有一种可能性(当更改 std: : string 和 std: : stringstream 的对象时) ,std: : bad _ alloc 异常会在其过程中被抛出,因此您无法准备或抛出(*)异常-您将丢失信息和状态。

在设计良好的程序中,在准备或处理异常时很容易避免内存分配。你只需要:

  • 要么在处理异常时保证该值仍然有效,并将某种类型的链接作为异常的一部分传递给该值——要么是引用,要么是某种类型的指针(最有可能是智能的) ,
  • 或在处理异常时使用异常类型 info 或/和固定大小值获取值; 比如说,
} 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());


}

当然,您总是可以选择接受缓冲区大小限制并使用预分配的缓冲区。

另外,请注意,用于异常的类型(类)不允许从它们的拷贝构造函数中抛出异常,因为如果初始异常试图被值捕获,就可以调用复制建构子(如果编译器没有省略的话) ,这个额外的异常会在初始异常被捕获之前中断初始异常处理,从而导致调用 std: : end。 由于 C + + 11编译器被允许在某些情况下在捕捉时消除复制,但是这两个省略并不总是合理的,如果合理的话,这只是允许而不是义务(详见 https://en.cppreference.com/w/cpp/language/copy_elision; 在 C + + 11之前,语言的标准并没有规范这个问题)。

此外,你应该避免异常(将调用他们的附加)被抛出构造函数和移动构造函数的类型(类)用于异常(将调用他们的初始) ,因为构造函数和移动构造函数可以调用时抛出的类型的对象作为初始异常,然后抛出一个额外的异常将阻止创建一个初始异常对象,和初始将只是丢失。除了来自复制建构子的额外异常外,当抛出初始的异常时,也会导致相同的结果。

如果是 C + + 14(operator ""s) ,则使用字符串文本操作符

using namespace std::string_literals;


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

或定义自己的,如果在 C + + 11。例如

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

然后您的 throw 语句将看起来像这样

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

看起来又干净又漂亮。

遇到了类似的问题,因为为自定义异常创建自定义错误消息会导致代码难看。这就是我的解决办法:

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

这将分离用于创建消息的逻辑。我最初想过覆盖 what () ,但是您必须在某个地方捕获您的消息。Run _ error 已经有一个内部缓冲区。

也许是这个?

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

它创建一个临时 ostringstream,根据需要调用 < < 操作符,然后将其包装在圆括号中,并调用。函数,将临时 std: : string 传递给 run _ error 的构造函数。

注意: ostringstream 和字符串是 r 值的临时值,因此在此行结束后超出范围。异常对象的构造函数必须使用复制语义或(更好的) move 语义获取输入字符串。

附加说明: 我不一定认为这种方法是“最佳实践”,但它确实有效,可以在必要时使用。最大的问题之一是这个方法需要堆分配,因此运算符 < < < 可以抛出。您可能不希望这种情况发生; 然而,如果您进入了这种状态,您可能有更多的问题需要担心!

每当我需要在异常中抛出自定义消息时,我用 snprintf()构造一个 C 样式的字符串,并将其传递给异常构造函数。

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

我不确定是否需要额外的字符串 string error_mesg(buffer)。我的理由是,buffer位于堆栈内存上,如果异常捕获器继续运行,那么允许捕获器保留对堆栈分配的 C 字符串的引用是有问题的。相反,将 string传递给异常将调用逐值复制,并且 buffer数组将被深度复制。