模拟 virtual 函数

之前 boost 提供了一个神奇的 type erasure 库,差点都忘掉了。这次碰到了一个类似的问题:

某个容器类 C<T> 支持 partition 方法,返回 std::vector<C<T>>,我们有几种 partition 方法实现的形式,每种形式对应了一种自己的 config 类型(这里使用 protobuf 定义这些 config,但不是必要的),现在希望提供一个统一的 interface 方便我们运行时切换实现不同的 partition。

最直接的 API 大约是

template <typename T, typename Config>
std::vector<C<T>> split(const C<T>& c, const Config& config);

这样一来我们要对每个支持的 Config 进行特化,将不同的实现与调用 partition 结合起来。同时这是一个 compile time 决定使用哪种实现的 API,不符合运行时切换的目标。因此我们可以通过一个 SplitConfig 作为所有可能 Config 的 facade,由它决定使用哪种实现,这在 protobuf 里面可以使用 oneof feature。于是似乎 API 简化成为了

template <typename T>
std::vector<C<T>> split(const C<T>& c, const SplitConfig& config) {
  auto split_method = config.split_method_case();
  switch (split_method) {
  case kSplitMethod1:
    return SplitMethod1(c, config.split_method1());
  case kSplitMethod2:
    // ...
  default:
    throw std::runtime_error("unsupported split method!");
  }
}

so far so good 可是一个简单的 split 却因为种种原因需要提供一些额外的信息给调用者(这里我们考虑为每个 split 提供一个 name)。相对于前面一种设计来说,这种情况下问题会变得更麻烦。(尽管这并不是很好的一种做法,本质上我们需要提供所有的特化,并且我们还需要更高 level 上进行 runtime 到 compile time 的转换)前面一个 API 可以对等的提供

template <typename Config>
std::vector<std::string> split_names(const Config& config);

而后者又得来一个 switch-case,我们每增加一种额外的信息(比如每个 split 最后存放的位置),我们就得多维护一个 switch-case 而且每增加一种 split 方法,我们几乎的更新所有的这种结构。OO 设计思想告诉我们,我们应该为此提供一个 base class

class splitter {
public:
  // template <typename T> virtual std::vector<C<T>> split(const C<T>& c) const;
  virtual std::vector<std::string> names() const;
};

如果说 names 是一个合理的 virtual function,问题就来了,我们的 split 方法因为输入的是个模版类,怎么能够变成一个 virtual function 呢?boost.TypeErasure 的设计告诉我们,我们可以将这个模版方法在父类里面定义,而其实现调用一个一致的接口,很遗憾这一点在这个问题上不能借鉴,因为返回类型仍然是一个模版。反倒是前面一种做法我们可以这样来做

template <typename T>
std::vector<C<T>> split(const C<T>& data, const SplitConfig& config) {
  switch (config.split_method_case()) {
  case kSplitMethod1:
    return split<SplitMethod1Config>(data, config.split_method1());
  case kSplitMethod2:
    // ...
  default:
    throw runtime_error("unsupported split");
  }
}

这里提供另一种思路,虽然 splitter 作为 base class 我们无法定义这个 template method,但是每个子类是可以定义的,我们通过 config 的类型能够很容易初始化一个 splitter 的子类,我们只需要将子类对应的这个方法调用就可以了,因此我们需要为 splitter 提供一个 factory method,另外需要将这个 splitter 运行时 downcast 成为它自己(来使用对应的 template method)。

class splitter {
public:
  static std::unique_ptr<splitter> create(const SplitConfig& config);
  virtual std::vector<std::string> names() const;
};

namespace internal {
  template <typename T>
  using TypeAndMethod = std::pair<std::type_index, std::function<std::vector<C<T>>(splitter*, const C<T>&)>>;

  template <typename T, typename Splitter> TypeAndMethod<T>
  get_type_and_method() {
    return {std::type_index(typeid(Splitter)), [](const splitter& s, const C<T>& data) {
      return dynamic_cast<const Splitter&>(s).Split(data);
    }};
  }
}

template <typename T>
std::vector<C<T>> split(const C<T>& data, const splitter& s) {
  static const std::unordered_map<std::type_index, std::function<std::vector<C<T>>(splitter*, const C<T>&)>> methods {
    get_type_and_method<T, simple_splitter>(), ...
  };
  return methods[std::type_index(typeid(*))](data);
}

template <typename T> std::vector<C<T>>
split(const C<T>& data, const SplitConfig& config) {
  auto s = splitter::create(config);
  // we can use virtual functions to get names
  auto names = s->names();
  // use the map for calling templated split
  return split(data, s);
}

这个地方有几点挺有意思的:

  • 为了获得事实上的 interface,每个 splitter 的实现都需要提供 split 模版,
  • 即便每个 split 实现可能不大一样,比如某种 splitter 只对 C<int> 有效,那么只提供了一个成员方法,只要调用这个 split facade 的时候给定的 type 为 int 还是可以 work 的。
  • 但是如果这样对于非 int 情况下产生的 methods 就会有问题,我们可以把 get_type_and_method 的实现调整一下,在方法不存在的时候对应的 function 换成 runtime failure(不知道 std::enable_if 是否能做这个),或者为 int 特化一下对应的 method 估计也行
  • 直接使用 type_info 不能作为 key,因此需要用 type_index
  • 对应的 map 应该可以作为单独的 static 变量怎么 register 一下,会比现在这样要方便一点

回想一下前面的做法,通过 typeid 将父类引用转换成为子类引用,通过一个静态的 map 根据类型静态的调用成员方法。这本质上就是模拟了一个 vtable,只是常规的 vtable 是编译器产生的,只支持非模版函数,而我们这里将其转换到了更一般的函数。

更直接的策略是把 splitter 弄成模版,而不是 split 方法,但是这样一来也意味着 names/paths 方法会被多次 copy.

——————
And Judah sent the kid by the hand of his friend the Adullamite, to receive his pledge from the woman’s hand:but he found her not

Advertisements
模拟 virtual 函数

发表评论

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