C++ 杂谈(8)

EBCO

所谓的 empty base class optimization 是指在基类为空时,C++ 标准虽然说 empty class 的 size 不能为 0,但是在继承的情况下,子类可以因此节约自己的 size(不扩大)。这在某些时候可以用来节省内存,比如如果需要 empty class 对象作为成员,一种策略就是将其作为 private 继承的基类,这样就可以省掉作为成员占用的内存了。

模板与继承

如果模板参数 T 作为模板类自身的父类存在,那么有一个很微妙的事情:如果模板类定义了一个方法,这个方法可以是或者不是基类的 virtual function,这时就会产生不同的现象,不知道这个有没有具体的用途。

CFI 及其使用

我们知道 C/C++ 在执行的时候依赖一个 call stack 作为函数调用的空间,它表征了程序执行的状态。在某些特定的时候我们很可能希望获得 call stack 的信息,这就是一个 compiler 为 call frame information(CFI)准备的 API,gcc 提供的相关 API 见此和这里的讨论。我们大可以将这个函数封装一下,比如在异常抛出或者碰到 signal 的时候用来打印相关调用栈信息供程序员调试。不过 g++ 的版本 ms 输出的比较难看,没有做 demangling,mac 上似乎已經自动的做好了。

std::terminate

这是一个类似 std::exit 的东西,通过 get_terminate、set_terminate 我们可以设置 terminate 时候的 handler,默认的实现是 call std::abort,如果我们希望在默认的 handler 之上先调用别的 handler,可以参考下面的 code

class terminator {
  static std::vector<std::terminate_handler> handlers;
  static std::vector<std::function<void()> > udfs;

  template
  friend void terminator_handler();

public:

  template
  struct helper {
    terminator::helper<N+1> push(const std::function<void()>& udf);
  };

  static helper stack() {
    return helper();
  }

};

template
void terminator_handler() {
  terminator::udfs[N]();
  terminator::handlers[N-1]();
}

template  terminator::helper<N+1>
terminator::helper::push(const std::function<void()>& udf) {
  terminator::handlers.push_back(&terminator_handler<N+1>);
  terminator::udfs.push_back(udf);
  std::set_terminate(handlers[N+1]);
  return terminator::helper<N+1>();
}

如此一来我们可以很 ws 的使用下面的代码在挂掉之前打印 traceback 信息

std::vector<std::terminate_handler> terminator::handlers {std::get_terminate()};
std::vector<std::function<void()> > terminator::udfs {[](){std::cout << "shouldn't be called!\n";}};
terminator::stack()
  .push([]() {
      stdout_backtrace_printer<> printer;
    });
std::terminate();

异常初步研究

C++ 乃至很多语言的异常并不是一个完全依靠语言本身实现的功能,通常它需要配套的 library 提供一些核心的东西:

  • throw 对应分配 exception 和开始 unwind call stack 两个部分,需要前者是因为 exception 的特殊性,需要在另外一个或者不存在的位置拿到这个对象,同时也应该不能在 heap 上分配(可能因为没内存了抛出的异常)
  • catch 的时候相当于在对应函数里面标注自己能捕获异常,同时我们发现 catch 要匹配类型,因此必须使用 RTTI
  • unwind 的过程分为两个阶段,其中一个仅仅用来搜索能够捕获抛出异常的 call frame,因此 unwind 肯定需要使用 CFI 相关的 API 定位 catch 嵌入在 stack 里面的信息,继而使用 RTTI 判断这个异常是否应该被该 catch 捕获,编译器会为含有 catch 的函数构造一张表,方便 unwind 进行相关的查询
  • 如果查询并没有发现合适的 exception 处理程序,就会调用 std::terminate
  • 如果发现了合适的 call frame,这时会进行第二轮的扫描,这时 unwind 会开始调用每个 call frame 需要的 destructor 释放对象一直到 catch 成功的代码,停止调用 destructor,进而把执行权交回给用户的代码

为什么需要两轮搜索?其实很简单,这主要是为了保持现场,在 std::terminate 调用时我们还有机会重新审视这个 call stack。

——————
And when Shechem the son of Hamor the Hivite, prince of the country, saw her, he took her, and lay with her, and defiled her

Advertisements
C++ 杂谈(8)

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s