如何在运行时确定是否存在特定C ++异常类的catch块?

时间:2011-08-09 13:23:29

标签: c++ exception introspection

在Linux上,我希望能够确定特定类异常(或该类的超类)的catch块当前是否在范围内(忽略捕获所有块)。

特别是,我希望能够实现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部分中,如this post中所述。但是,我不确定程序是否有任何简单的方法来解释该信息。有人可以帮忙吗?

6 个答案:

答案 0 :(得分:4)

阅读您的一条评论,我看到您想要这样做的一个原因是避免为处理的异常生成回溯,但是未处理的异常,您希望进行回溯。

如果这就是你想要这样做的原因,你可以使用std::set_terminate()来设置终止处理程序,当发生未处理的异常时会调用它。使用我自己的回溯处理程序进行测试,回溯显示跟踪一直到导致失败的throw(),因为throw实际上不会捕获异常后直接调用终止处理程序。

注意,这只会从最近一次投掷中捕获堆栈,直到它终止。如果你捕获然后重新抛出异常,那么初始throw和catch之间的堆栈将被解开并且不再可用。

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

答案 1 :(得分:2)

你可以通过制作一个“金丝雀”过程来测试发生的事情并报告结果,从而以一种非常流行的方式解决这个问题。我把一个“概念证明”的例子放在一起:

#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静态成员 - 你可能需要在金丝雀进程运行时使任何其他线程挂起。

免责声明:这样做可能是一个坏主意。

答案 2 :(得分:2)

我的想法如下。请记住,我正在为此动态编写所有代码,因此可能并不完美。 :)

实际上在代码中解释可能更容易,但我会先尝试给出要点。由于你对catch (...)不感兴趣,我没有专注于检测,但我认为修改这个想法也是相对容易的。 (注意:最初我打算使用一个指向函数的指针作为判断你所处的函数的方法,但我最终得到了这个名字,因为我没有考虑虚函数。我相信这可以如果有必要,所有这些都会得到优化。)

创建以下内容:

  • 具有所需功能的静态版本的课程
  • 一种特殊的自定义“堆栈”类型,用于保存您的异常信息,其操作可以根据该信息拆除堆栈
  • 包含类型字符串和 void指针字符串的结构,用于保存在
  • 中创建的函数的名称

设定:

  • try之前,放置一个捕获类型结构,其中包含要捕获的类型名称,以及堆栈中此函数中捕获的所有异常,以及指向的指针功能名称。
  • 要为类型设置的字符串是通过引用类型来确定的(因此"..."适用于默认捕获)。
    • 最初我正在尝试使用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从中提取构建文件的目录,然后在编译之前处理所有文件并将输出放在新的build子目录中。我打赌LINQ可以在一些小语句中做到这一点,虽然这并不意味着可以编写LINQ。 :)我仍然打赌它不是那么大的任务,并且它将是实现解决方案的完美方式;在类中定义堆栈,类和静态检查器函数。

这让我想起......“完整性”:

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

所以,结束时:我很难想象处理像你最终拥有宏的代码一样的代码。现在,我没有缩进,我想它会变得半更半可读,但问题刚刚发生了变化:现在如果你有七个异常类型被处理,你有7个缩进将所有内容推离屏幕。但我确实认为可以通过一个简单的外部应用程序完成一些工作,它可以自动为您完成。

答案 3 :(得分:1)

我不知道你怎么能检查存在哪些catch块,它可能需要付出很大的努力,如果你改变了相同编译器的次要版本,它可能会中断。您的注释表明您真正想要的是在异常的构造函数中获取堆栈跟踪,而不是它们被捕获的位置。

使用backtracebacktrace_symbols函数在linux / gcc中实际上很容易做到GCC很乐意为您提供堆栈转储。另请参阅:the man pagethis SO question(请注意问题是关于崩溃,但您可以随时在程序中执行此操作,无论崩溃与否)。

即使异常被某些代码(或其他)捕获,它仍会生成堆栈转储,但它会让代码继续运行而不是调用abort()或terminate()。但你可以通过日志查看哪一个导致你的问题,你不应该有那么多(如果你这样做,你可能使用异常错误...他们是if / else / while或者返回的可怜的替代品有时会出现错误代码,

答案 4 :(得分:0)

“我知道系统有这些信息,以便它能正确实现异常处理”

这是不真实的。系统可以为异常处理程序使用真正的堆栈,即只能访问顶级异常处理程序的堆栈。在C ++中,您永远不需要知道是否存在另一个异常处理程序,然后才能确定是否输入了顶级异常处理程序。抛出的异常由顶级处理程序处理,并且您使用它,或者它没有被处理,并且您弹出顶部异常处理程序未使用。

在您的情况下,运行时可能看到的唯一异常处理程序是catch( MyException )。这意味着您无法知道isThereACatchBlock( typeid( MyOtherException ) );应该是false。访问异常处理程序“catch( MyException )”的唯一方法可能是throw catch( MyException )未处理的异常。

答案 5 :(得分:-1)

你可以玩重新抛出......

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

}