Как я могу определить во время выполнения, есть ли блок catch для определенного класса исключений C ++? - PullRequest
3 голосов
/ 09 августа 2011

В Linux я хотел бы иметь возможность определить, находится ли блок перехвата для определенного класса исключений (или суперкласса этого класса) в настоящее время (игнорируя перехват всех блоков).

В частности, я хотел бы иметь возможность реализовать функцию isThereACatchBlock, которая будет вести себя следующим образом:

bool isThereACatchBlock( std::type_info const & ti ) {
    ...;
}

class MyException {
};

class MyDerivedException : public MyException {
};

class MyOtherException {
};

void f() {
    try {
        isThereACatchBlock( typeid( MyException ) ); // Should return true
        isThereACatchBlock( typeid( MyDerivedException ) ); // Should return true
        isThereACatchBlock( typeid( MyOtherException ) ); // Should return false
   } catch( MyException const & e ) {
   } catch( ... ) {
   }
}

Я знаю, что система обладает этой информацией, чтобы она могла правильно реализовать обработку исключений - я полагаю, что она хранится в разделах .eh_frame и / или .gcc_except_table, как описано в в этом посте . Тем не менее, я не уверен, существует ли какой-либо простой (-иш) способ для программы интерпретировать эту информацию. Кто-нибудь может помочь?

Ответы [ 6 ]

4 голосов
/ 16 августа 2011

Читая один из ваших комментариев, я увидел, что одна из причин, по которой вы хотите это сделать, состоит в том, чтобы избежать создания обратной трассировки для обработанных исключений, но для необработанных исключений вы хотите иметь обратную трассировку.

Если именно поэтому вы хотите это сделать, вы можете использовать std::set_terminate() для установки обработчика завершения, который вызывается при возникновении необработанного исключения. При тестировании с моим собственным обработчиком backtrace backtrace показывает трассировку вплоть до throw (), вызвавшего сбой, так как throw в основном вызывает обработчик завершения напрямую, после того, как осознает, что исключение не будет перехвачено.

Обратите внимание, это захватывает стек только с самого последнего броска, пока он не завершится. Если вы поймаете, а затем повторно сгенерируете исключение, стек между начальным броском и уловом будет размотан и больше не доступен.

int foo() {
  throw std::runtime_error("My error");    
}
std::terminate_handler old_term_func;
void term_func() {      
  // Insert backtrace generation/output here
  old_term_func();
}

void test() {
  try {
    foo();
  } catch (const std::runtime_error& e) {
    std::cout <<"Exception caught, no backtrace generated" << std::endl;
  }    
  foo(); // Exception not caught, will call term_func    
}
int main() {
  old_term_func = std::set_terminate( term_func ) ;
  test();
}
2 голосов
/ 20 августа 2011

Моя мысль заключается в следующем.Пожалуйста, имейте в виду, что я пишу весь код для этого на лету, чтобы он не был идеальным.:)

На самом деле это, вероятно, легче объяснить в коде, но я сначала попытаюсь дать суть.Поскольку вы не заинтересованы в catch (...), я не сосредоточился на обнаружении этого, однако, я думаю, что было бы относительно легко изменить идею, чтобы справиться и с этим.(Примечание: изначально я собирался использовать указатель на функцию как способ сказать, в какой функции вы находитесь, но в итоге я выбрал имя, потому что не думал о виртуальных функциях. Я уверен, что это можетвсе они должны быть оптимизированы, если необходимо.)

Создайте следующее:

  • класс со статическими версиями ваших желаемых функций
  • специальный пользовательский тип "стека" дляхранить информацию об исключении с операциями, которые могут разрушить стек, основываясь на этой информации
  • структура, содержащая строки типа и строку void pointer для хранения имени функции, в которой она была создана

Настройка:

  • до вашего try, поместите структуру типа catch с именем перехваченного типа, а также со всеми исключениями, перехваченными в этой функции, настек вместе с указателем на имя функции.
  • строка для типа определяется путем цитирования типа (так что "..." подходит для значения по умолчаниют ловит).
    • Первоначально я играл с идеей использовать typeid для получения недокорированных имен типов, затем использовать .raw_name() для получения искаженного имени
    • , но это не будет работать для нативныхтипы, или для не виртуальных типов, и в действительности нет необходимости в искаженных именах, так что это немного бессмысленно для этой реализации

Разрыв:

  • в каждом catch блоке, наверху сорвите стек на единицу больше, чем тот, который вы перехватываете для функции, в которой вы находитесь
  • после последних catch блоков в функции, порвитесложите один после первого случая разрыва в последнем улове

Основная проблема этого решения заключается в том, что оно явно очень громоздко.

Одним из решений является [поспорить, что вы видели это прибывающее] макросы.

ExceptionStackHandler.h

// ... 
// declaration of the class with the needed functions, perhaps
// inline definitions. the declaration of the stack. etc.
// ...
#if __STDC__ && __STDC_VERSION__ >= 199901L
    #define FN_NAME __func__
#else
    #define FN_NAME __FUNCTION__
#endif

// was thinking would be more to this; don't think we need it
//#define try_code(code) try { code } 

// this macro wraps the code such that expansion is not aborted 
// if there happen to be commas in the code.
#define protect(code) if (true) { code }

// normal catch and processing
#define catch_code(seed_code, catch_type, catch_code) \
    ExceptionStackHandler.Stack.Push(exceptionItem(#catch_type, FN_NAME)); \
    seed_code \
    catch (catch_type Ex) \
    { \
        ExceptionStackHandler.Stack.PopThrough(#catch_type, FN_NAME); \
        catch_code \
    } 

// you *must* close a try with one of the following two calls, otherwise
// some items may be missed when clearing out the stack

// catch of all remaining types
#define close_catchall(seed_code, last_catch_type, catch_code) \
    seed_code \
    catch (...) \
    { \
        ExceptionStackHandler.Stack.PopThrough(#last_catch_type, FN_NAME); \
        catch_code \
    } \
    ExceptionStackHandler.Stack.PopThrough(#last_catch_type, FN_NAME); \

// cleanup of code without catching remaining types
#define close_nocatch(last_catch_type, catch_code) \
    seed_code \
    ExceptionStackHandler.Stack.PopThrough(#last_catch_type, FN_NAME)

Тогда в вашем коде это будет выглядеть как

bool isTheRoofOnFire(bool& isWaterNeeded)
{
    // light some matches, drip some kerosene, drop a zippo

    close_nocatch
    (
    catch_code
    (
    catch_code 
    (
    //--------------------------------------------------------
    // try block for the roof
    try
    {
        protect (
            // we don't need no water
            if (isWaterNeeded)
                isWaterNeeded = false;
        )
    }
    // End Try Block
    //--------------------------------------------------------
    ,
    //--------------------------------------------------------
    // catch(string Ex)
    string,
    protect (
      if (Ex == "Don't let it burn!")
          isWaterNeed = true;

      throw "I put the water on the fire anyway.";
    )
    )
    // END - catch (string Ex) 
    //--------------------------------------------------------
    ,
    //--------------------------------------------------------
    // catch(RoofCollapsedException Ex)
    RoofCollapsedException
    try_code (
        protect (
            if (RoofCollapsedException.isAnythingWeCanDo == false)
                throw new TooLateException(RoofCollapsedException);
            else
                isWaterNeeded = true;
        )
    )
    // END - catch(RoofCollapsedException Ex)
    //--------------------------------------------------------
    )
    // closing without catchall exception handler
    //--------------------------------------------------------
}

Теперь, я признаю, это ужасно.Reeeal некрасиво.Я уверен, что есть лучший способ написать эти макросы, но как теоретическое подтверждение концепции, я не думаю, что там есть что-то, что не работает.Но настоящее решение не может быть таким сложным.Причина, по которой это не получается, не в том, что идея такая безобразная.Просто макросы не могут реализовать это чистым способом.Поскольку это такой обычный шаблон, просто должен быть способ сделать это, даже не касаясь вашего исходного кода.Если бы только препроцессор C не был единственным выбором ...

;) Итак.На самом деле я думаю, что может быть.Лучшим решением является использование более мощного препроцессора, который дает более чистый код C ++, позволяя вам компилировать даже без дополнительной предварительной обработки (например, директивы в качестве комментариев).Я думаю, что было бы довольно легко написать что-нибудь, используя инструмент, подобный CS-Script (который будет работать под Mono), и я полагаю, что некоторые примеры включены в документацию процесса 'прекомпилятора', который позволяетты делаешь это.И действительно, для этого: вам даже не нужны директивы.Директивы были бы хороши, но вам не нужен универсальный макропроцессор, чтобы делать то, что вам нужно.Конечно, само собой разумеется, что вы можете написать это во всем, что имеет возможность обрабатывать текстовый файл.

Хотя я еще не пытался реализовать его, я думаю, что это может быть просто процессор, который работает на целой группе файлов, которые не требуют каких-либо изменений непосредственно в коде. (Найдите все блоки try/catch в файле, соберите типы, создайте дополнительные операторы и запишите файл.) Возможно, переместите каталог, из которого Makefile извлекает файлы сборки, затем перед компиляцией обработайте все файлы и поместите вывод в новый подкаталог сборки. Могу поспорить, что LINQ может сделать это в нескольких небольших заявлениях, хотя это не означает, что I может написать LINQ. :) Я все еще держу пари, что это не будет такой большой задачей, и это будет идеальный способ реализовать решение; определить стек, класс и функции статической проверки в классе.

Что мне напоминает ... за "полноту":

bool ExceptionStackHandling::isThereACatchBlock(string Type)
{
    return (ExceptionStackHandling.Stack.peekOnType(Type) > 0);
}

Итак, в заключение: я с трудом могу себе представить, что мы имеем дело с кодом, подобным коду, который у вас получается с макросами. Теперь я не делал отступ, и полагаю, что он станет почти наполовину более читабельным, но проблема только что переместилась: теперь, если вы обрабатываете семь типов исключений, у вас есть семь отступов, выталкивающих все с экрана. Но я думаю, что что-то можно сделать с помощью простого внешнего приложения, которое делает все это автоматически для вас.

2 голосов
/ 15 августа 2011

Вы можете решить эту проблему очень хитрым способом, сделав «канарейку», которая проверяет, что происходит, и сообщает о результате.Я собрал пример «доказательства концепции»:

#include <exception>
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>

struct SomeException : public std::exception {
};

template <typename E>
struct isThereACatch {
  isThereACatch() : result(doTest()) {
  }

  operator bool() const {
    return result;
  }

private:
  const bool result;

  struct TestException : public E {
    TestException(int f) {
      fd = f;
    }

    virtual ~TestException() throw() { 
      notify(true);
      exit(0);
    }

    static void notify(bool result) {
      const ssize_t ret = write(fd, &result, sizeof(result));
      assert(sizeof(bool) == ret);
    }

    static void unhandled() {
      notify(false);
      exit(0);
    }

    static int fd;
  };

  static bool doTest() {
    int pipes[2];
    const int ret = pipe(pipes);
    assert(!ret);
    const pid_t pid = fork();
    if (pid) {
      // we're parent, wait for the child to return
      bool caught;
      const ssize_t ret = read(pipes[0], &caught, sizeof(caught));
      assert(sizeof(bool) == ret);
      int status;
      waitpid(pid, &status, 0);
      return caught;
    }
    else {
      // if we are the child (i.e. pid was 0) use our own default handler
      std::set_terminate(TestException::unhandled);
      // Then throw one and watch
      throw TestException(pipes[1]);
    }
  }
};

template <typename E>
int isThereACatch<E>::TestException::fd;

int main() {
  try {
    isThereACatch<std::exception> e1;
    isThereACatch<SomeException> e2;
    std::cout << "std::exception - "  << e1 << std::endl;
    std::cout << "SomeException - " << e2 << std::endl;
  }
  catch (const SomeException& ex) {
  }
  std::cout << "Still running..."  << std::endl;
}

Преимущество в том, что он полупереносной.Буду ли я использовать это в гневе, хотя?Возможно нет.Моя большая обеспокоенность состояла бы в том, что могут быть значительные побочные эффекты от некоторых исключений, которые делают странные и удивительные (но неожиданные) вещи.Например, деструктор, который удаляет файл или хуже.Вам также необходимо убедиться, что тестируемые исключения являются конструируемыми по умолчанию, а не примитивными типами.Другая проблема с моим примером - это безопасность потоков, а не просто тривиальный fd статический член TestException - вам, вероятно, потребуется приостановить любые другие потоки, пока выполняется канарский процесс.

Отказ от ответственности: Выполнениеэто, вероятно, плохая идея.

1 голос
/ 09 августа 2011

Я не знаю, как вы могли бы проверить, какие блоки catch существуют, это, вероятно, потребовало бы значительных усилий и, вероятно, сломалось бы, если бы вы изменили даже второстепенные версии того же компилятора.Ваш комментарий указывает, что вы действительно хотите получить трассировку стека в конструкторах ваших исключений, а не в месте, где они были захвачены.

Это на самом деле легко сделать в linux / gcc с помощью функций backtrace и backtrace_symbols, которые GCC с радостью даст вам дамп стека.См. Также: справочную страницу и этот вопрос SO (обратите внимание, что вопрос касается сбоев, но вы можете сделать это в любой момент в вашей программе, независимо от того, происходит ли сбой или нет).

Это все равно будет генерировать дампы стека, даже если исключение было перехвачено каким-либо фрагментом вашего кода (... или иным образом), но это позволит коду продолжать работать, а не вызывать abort () или terminate ().Но вы можете просмотреть журналы, чтобы найти, какая из них привела к вашей проблеме, у вас их не должно быть много (если вы делаете, вы, вероятно, неправильно используете исключения ... они являются плохой заменой if / else / while или возвращениеминогда код ошибки),

0 голосов
/ 09 августа 2011

«Я знаю, что система обладает этой информацией, чтобы она могла правильно реализовать обработку исключений»

Это не соответствует действительности.Система может использовать настоящий стек для обработчиков исключений, то есть стек, в котором вы можете получить доступ только к верхнему обработчику исключений.В C ++ в его нынешнем виде вам никогда не нужно знать, существует ли другой обработчик исключений, прежде чем решать, будет ли введен верхний.Либо сгенерированное исключение обрабатывается верхним обработчиком, и вы его используете, либо оно не обрабатывается, и вы открываете верхний обработчик исключений.

В вашем случае единственный обработчик исключений, который может быть виден во время выполненияПоэтому catch( MyException ).Это означает, что вы не можете знать, что isThereACatchBlock( typeid( MyOtherException ) ); должно быть false.Единственный способ получить доступ к обработчику исключений «за» catch( MyException ) - это throw исключение, не обработанное catch( MyException ).

0 голосов
/ 09 августа 2011

Вы можете играть с перебросом ...

void isThereACatchBlock(bool& p_ret)
{
    try
    {
        throw;
    }
    catch(const MyException& p_ex)
    {
        p_ret = true;
        throw;
    }
    catch(const MyOtherException& p_ex)
    {
        p_ret =false;
        throw;
    }
    catch(...)
    {
        p_ret = false;
        throw;
    }

}

...