场景:
我们有一个具有通用错误处理接口(interface)的 API。它是一个 C API,所以它告诉我们在调用每个 API 函数之后,我们需要执行一些像这样的样板文件:
if (APIerrorHappened()) {
APIHandle h;
while(h = APIgetNextErrorContext(...)) {
cerr << APIgetText(h) << endl;
}
}
您显然讨厌重复自己,因此您希望将这种处理封装在一个宏中,以便您可以编写如下代码:
...
// this API call returns something
APItype foo = MYMACRO(APIsomeFunction1(...));
// this one doesn't
MYMACRO(APIsomeFunction2(...));
// neither does this (glCompileShader)
MYMACRO(APIsomeFunction3(...));
...
您还可以从面向方面的编程角度来考虑这一点 - 想象宏添加日志记录,将信息发送到远程监视器,等等......重点是它应该封装一个表达式,做任何事情围绕它,并返回表达式返回的任何类型 - 当然表达式可能不会返回任何。
这意味着你不能这样做
#define MYMACRO(x) { auto x = expr(); ... }
...因为在某些情况下,表达式不会返回任何内容!
那么...你会怎么做?
请不要建议将完整的语句封装在宏中...
#define MYMACRO(x) \
{ \
/* ... stuff ... */ \
x; \
// ... stuff
}
...因为这对于以下内容永远不起作用:
if (foo() || APIfunctionReturningBool(...) || bar()) {
...
APIfunction1();
...
} else if (APIOtherfunctionReturningBool() || baz()) {
...
APIfunction2();
...
}
...你吞没了所有的 if 语句吗?它的操作包括其他 API 调用,所以...宏中宏?调试简直变成了 hell 。
我自己的尝试如下,使用 lambdas 和 std::function - 但它可以说是丑陋的...... 我无法将表达式的 lambda 直接传递给采用 std::function 的模板(根据 lambda 的返回类型进行专门化),因此代码变得相当糟糕。
你能想到更好的方法吗?
void commonCode(const char *file, size_t lineno) {
// ... error handling boilerplate
// ... that reports file and lineno of error
}
template <class T>
auto MyAPIError(std::function<T()>&& t, const char *file, size_t lineno) -> decltype(t()) {
auto ret = t();
commonCode(file,lineno);
return ret;
}
template<>
void MyAPIError(std::function<void(void)>&& t, const char *file, size_t lineno) {
t();
commonCode(file,lineno);
}
template <class T>
auto helper (T&& t) -> std::function<decltype(t())()>
{
std::function<decltype(t())()> tmp = t;
return tmp;
}
#define APIERROR( expr ) \
return MyAPIError( helper( [&]() { return expr; } ), __FILE__, __LINE__);
更新,KennyTM 优秀解决方案的附录
我放置了触发此问题的实际 OpenGL 代码 here 。正如您所看到的,错误检查代码不仅仅是打印 - 它还引发了用户代码可以处理的异常。我添加此附录是为了注意,使用 KennyTM 的解决方案,您最终会从析构函数中抛出此异常,并且这是可以的(继续阅读):
struct ErrorChecker {
const char *file;
size_t lineno;
~ErrorChecker() {
GLenum err = glGetError();
if (err != GL_NO_ERROR) {
while (err != GL_NO_ERROR) {
std::cerr <<
"glError: " << (char *)gluErrorString(err) <<
" (" << file << ":" << lineno << ")" << std::endl;
err = glGetError();
}
throw "Failure in GLSL...";
}
}
};
可以从该析构函数中抛出的原因,在 C++ FAQ 中进行了解释。 :
The C++ rule is that you must never throw an exception from a destructor that is being called during the "stack unwinding" process of another exception... you can say never throw an exception from a destructor while processing another exception.
在我们的例子中,我们希望用户代码(调用特殊宏)来处理异常;因此我们需要确定 ErrorChecker 析构函数中的“抛出”是第一个 - 即实际调用的 C API 永远不会抛出。使用此表单可以轻松完成此操作:
#define GLERROR(...) \
([&]() -> decltype(__VA_ARGS__) \
{ \
ErrorChecker _api_checker {__FILE__, __LINE__}; \
(void) _api_checker; \
try { \
return __VA_ARGS__; \
} catch(...) {} \
} ())
这种形式的宏保证实际的 C API(通过 VA_ARGS 调用)永远不会抛出异常 - 因此,ErrorChecker 析构函数中的“抛出”始终是第一个 这样做。
所以这个解决方案涵盖了我原来问题的所有角度 - 非常感谢 Alexander Turner提供它。
最佳答案
将日志记录代码放入某个类的析构函数中(假设记录器不会抛出异常),然后在宏中创建该类的实例。滥用逗号运算符,我们有:
struct Logger
{
const char* file;
int lineno;
~Logger()
{
// The APIerrorHappened stuff.
}
};
#define APIERROR(...) (Logger{__FILE__, __LINE__}, (__VA_ARGS__))
关于C++11 lambda 作为 std::function 参数传递 - 根据返回类型进行调度,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9688767/