我正在为不同制造商的几种设备开发编程接口。大多数制造商通常生产至少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_if
与std::enable_if
的不同之处在于,它接受类型作为第一个参数,而不是布尔值。因此,可以使用boost::enable_if_c
,它的工作原理就像std::enable_if
或使用boost::enable_if
与boost::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这样的静态分析器,您可以在其中添加规则来分析代码,并在违反规则时抛出相应的错误。希望这对你有帮助!