Сценарий:
У нас есть 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, так что ... макрос внутри макроса? Отладка просто превратилась в ад.
Моя собственная попытка ниже, с использованием lambdas и std :: function - но это, возможно, уродливо
Я не мог передать лямбда-выражение непосредственно в шаблон, который принимает std :: function (чтобы специализироваться на основе типа возвращаемого лямбда-выражения), поэтому код оказался довольно неприятным.
Можете ли вы придумать лучший способ?
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, который вызвал этот вопрос здесь . Как видите, код проверки ошибок сделал больше, чем просто напечатал - он также выдал исключение, которое пользовательский код мог затем обработать. Я добавляю это дополнение, чтобы отметить, что с решением 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 :
Правило C ++ состоит в том, что вы никогда не должны выбрасывать исключение из деструктора, который вызывается во время процесса «разматывания стека» другого исключения ... вы можете сказать, никогда не выбрасывать исключение из деструктора при обработке другого исключение .
В нашем случае мы хотим, чтобы код пользователя (который вызывает специальный макрос) обрабатывал исключение; поэтому нам нужно точно знать, что наш «throw» в деструкторе 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 ) никогда не сгенерирует - и, следовательно, «throw» в деструкторе ErrorChecker всегда будет первым Делать это.
Так что это решение охватывает все аспекты моего первоначального вопроса - большое спасибо Александру Тернеру за его предоставление.