类方法子集的编译时生成

Compile-time generation of a subset of class methods

本文关键字:编译 子集 类方法      更新时间:2023-10-16

我正在为不同制造商的几种设备开发编程接口。大多数制造商通常生产至少12种型号。命令和数据作为低级指令发送到设备。问题是,虽然没有两个设备支持相同的指令集,但大多数设备支持的指令集之间存在相当大的重叠。

因为低级指令很奇怪,我计划将它们包装在直观命名的类方法中,这样我在编写或读取(或调试)代码时不必查找文档。在我的设计的第一个版本中,所有的方法都属于一个Device类,它的构造函数接受一个参数,该参数是一个枚举,表示设备的型号。例如:

  class Device
  {
  public:
       enum Model{ ABC , KLM , XYZ };
       Device( Model _model );        // ctor
       // Commands (encapsulate low-level instructions)
       inline void do_Foo();           // supported by all models
       inline void do_Bar();           // unsupported by 'KLM'
  };

但是,另外,如果初始化Device的模型不支持命令方法,我希望防止调用命令方法。事实上,如果为设备模型KLM调用do_Bar(),我想生成编译时错误。我已经排除了为每个设备型号创建一个类的可能性,因为这将涉及创建大量的类。

我考虑使用预处理器指令#error,以便使用当前设备模型作为谓词或先决条件来生成编译时错误,尽管我不确定预处理器#if..宏是否支持非常量,例如我的设备模型。在理想情况下,命令方法将被标记为支持的方法,因此允许调用它。然而,我希望我没有要求太多,我希望这能尽可能容易地完成,这样添加对新设备的支持就相对简单,并且不涉及太多(容易出错的)编辑。

后记:我意识到by设计可能有缺陷,因为所有的方法都应该是可调用的。我想,使用STL仍然可以为每个设备生成有效命令的子集,尽管我不知道哪种STL范例(例如trait)适用于这种情况。

你不能对只在运行时才知道的东西(比如传递给编译器的参数)做编译时的决定。因此,您基本上有两个选项:

1)在运行时调用不支持的方法时抛出异常

inline void do_Bar(){
  if(model == KLM) throw runtime_exception("do_bar unsupported by device");
  ...
}

2)创建许多类,可能通过只包含适当方法的模板。一种方法是:

  enum Model{ ABC , KLM , XYZ };
  template<Model M>
  class Device {
  public:       
       Device();        // ctor
       // Commands (encapsulate low-level instructions)
       inline void do_Foo();           // supported by all models
       template<Bool Dummy = true>
       inline typename std::enable_if<Dummy && (M != KLM), void>::type do_Bar(); // unsupported by 'KLM'
  };

模板参数Dummy是必需的,因为enable_if依赖于SFINAE,只有当方法本身是模板方法时才会起作用,而enable_if依赖于模板参数。因为它是默认的模板参数,所以在调用方法时不需要显式地提到它,所以

Device<ABC> d;
d.do_bar();

仍然可以工作(所以那里的接口没有变化)。

我使用std::enable_if,它只在c++ 11上可用,如果你没有,你需要使用boost::enable_if,或者自己写(这并不难)。

第二个选项的缺点是不可能编写不知道底层模型的代码。有利的一面是,它允许您通过部分专门化(或使用enable_if)掩盖所提供接口中的细微差异,从而为不同的模型获得不同的实现。

boost::enable_ifstd::enable_if的不同之处在于,它接受类型作为第一个参数,而不是布尔值。因此,可以使用boost::enable_if_c,它的工作原理就像std::enable_if或使用boost::enable_ifboost::integral_constant结合(这是Boost类型特征的一部分,所以包括boost/type_traits.hpp):

template<Bool B> typename boost::enable_if<boost::integral_constant<bool, B && (M != KLM)>, void>::type do_bar();

您所需要的本质上是静态多态性:根据类的类型改变其编译时属性。要做到这一点,您需要将运行时模型检查替换为使用类型的编译时检查。只需创建一堆类,每个类对应一个模型,并使用继承来共享公共代码。

template<class Model>
class Device {
protected:
    void do_foo();
    void do_bar();
};
class ModelABC : public Device<ModelABC> {
};
class ModelKLM : public Device<ModelKLM> {
private:
    void do_bar(); // not available for this model, private!
};
class ModelXYZ : public Device<ModelXYZ> {
};
//-------- common implementation for all models
template<class Model>
void Device<Model>::do_foo() {
    std::cout << "Device::do_foo()n";
}
template<class Model>
void Device<Model>::do_bar() {
    std::cout << "Device::do_bar()n";
}
//-------- special implementation of method do_foo() for model XYZ
template<>
void Device<ModelXYZ>::do_foo() {
    std::cout << "special implementation of do_foo() for model XYZn";
}
void test() {
    ModelABC abc;
    ModelKLM klm;
    ModelXYZ xyz;
    abc.do_foo();
    klm.do_foo();
    xyz.do_foo();
    abc.do_bar();
    //klm.do_bar(); // compile-time error!
    xyz.do_bar();
}

注意,您可以通过专门化相关方法的模板来实现任何特定于模型的行为。还可以通过private修饰符使模型的某些方法在编译时不可访问。


编辑

以一种更具有声明性的方式,您可以使用私有继承来表示每个模型中哪些方法可用,而不是哪些方法不可用。考虑以下代码:
template<class Model>
class Device {
protected:
    void do_foo();
    void do_bar();
};
class ModelABC : private Device<ModelABC> {
public:
    using Device<ModelABC>::do_foo;
    using Device<ModelABC>::do_bar;
};
class ModelKLM : private Device<ModelKLM> {
public:
    using Device<ModelKLM>::do_foo;
};
class ModelXYZ : private Device<ModelXYZ> {
public:
    using Device<ModelXYZ>::do_foo;
    using Device<ModelXYZ>::do_bar;
};

这个代码片段相当于前面的代码片段:模型KLM没有do_bar()方法,模型XYZ有专门的do_foo()方法。

考虑上面的Device类。当你编译这个类的时候,编译器会创建一个有足够空间的数据块来分配数据成员,而对于函数,它会创建名字被修改过的函数,并给它们添加一个新参数,这就是"this"指针。

构造函数也只是编译器的另一个函数,尽管语义分析器会注意到它没有返回类型,但它只是另一个函数,即在某个内存地址准备执行的指令列表。当函数被生成为机器码时,编译器所做的就是吐出指令来建立一个堆栈来保存参数,吐出实际的函数代码并清除堆栈。它不知道可能存储在参数中的值,因此它不能抛出错误!

也就是说,您可以考虑使用像clang这样的静态分析器,您可以在其中添加规则来分析代码,并在违反规则时抛出相应的错误。希望这对你有帮助!