模板化转换器和哈希器
关于模板和设计的一些想法。
引言
这篇文章是学习模板和一些设计思想的一个很好的实践。
背景
作为一个年轻的程序员(即使现在也是),我经常犯一个错误:在管理设备数组时,通常设备的ID范围是从 1..N,而数组存储的设备范围是从 0..N-1;这个简单的偏移量常常会制造麻烦。很多时候,你会看到如下代码:
m_ArrDevices[pDevice->DeviceId] ->DoSomething()
这段代码的问题在于,设备的ID是1,但数组中的位置应该是0,而我们却试图访问第二个位置的设备。最常见和简单的解决方案是在使用 DeviceId
时减去1,但这个解决方案并非没有错误,而且时不时地显得很难看。另一个问题 arises 是,当这两个世界(设备ID和设备位置)不像这样容易转换时会发生什么?如果这是一个非常复杂的哈希机制呢?由谁来负责将ID转换为位置?
分析一下
让我们看一个简单的例子并稍加分析
const int NUMBER_OF_DEVICES = 90;
class cDevice{}; //A dummy
cDevice arrDevices[NUMBER_OF_DEVICES];
//(1)
cDevice& returnDevice(int DeviceNumber)
{ //(2)(3)
return *(arrDevices[DeviceNumber-1]);
}
int main()
{
cDevice* tmpDevice= arrDevice[0];
// Do things with the Device
//(4)
returnDevice(tmpDevice->DeviceID).Work();
return 0;
}
这个简单的例子引出了一个最重要的普遍性问题:我们是否需要寻找一种方法来泛化这个问题?如你所见(2),问题得到了解决!答案取决于整体情况:如果这只是你唯一一次需要调整设备偏移量,你可以放松,或者将其提取到一个函数中,并在需要时修改它。但如果这是一个20人项目的一部分,长期使用,或者有一个不断变化的偏移量调整函数,甚至一个哈希函数,你可以肯定,有人会忘记应用它,误用它,甚至过度使用它!所以你需要一个强制进行转换的机制,但又希望它透明且灵活。让我们开始工作。首先,让我们分析我们的例子
- 我们的
DeviceNumber
是int
,但它可能不同,从一种 P.O.D. 类型到一个类。 - 我们的偏移量调整函数可以是任何东西,从一个简单的数学方程到一个复杂的哈希函数。
- 在这个例子中,我们接收一个
int
- 应用一个数学运算
- 返回一个
int
- 再次,但如果我们有一个接收字符串的字典,对其应用哈希函数,返回一个索引类呢?
我们需要泛化接收值、转换函数和索引类型的类型。考虑到这些,我们已经可以想象我们的模板了
//remeber! there is no difference between
//class and a typename in a template definition
template < typename RetPar, typename TypePar , class FCPar >
class transformationTemplate
{
// class implementation
};
在上面的例子中,transformationTemplate
的特化将是 transformationTemplate<int,int, blank>
(稍后我们将填补空白)。如果我们希望转换是透明的,我们需要它能够像它封装的类型一样行为,在我们的例子中是 int
;所以让我们向我们的类添加所需的函数
- 复制构造函数,接收一个
RetPar
类型的值。 - 复制赋值运算符,用于
RetPar
类型的值。 - 三法则规定,如果你显式定义了以上两个,你也必须定义析构函数。
我们的代码将是这样的
template < typename RetPar, typename TypePar , class FCPar >
class transformationTemplate
{
public:
transformationTemplate(TypePar const& newValue):m_value(newValue) {}
TypePar const& operator =
(TypePar const& newValue) { m_value = newValue; return newValue; }
virtual ~transformationTemplate() {}
private:
TypePar m_newValue;
};
(A,B) 复制构造函数和赋值运算符使我们能够向用户隐藏他正在使用一个类而不是他之前使用的那个数据类型的事实;我们将变量通过 const 引用传递给 TypePar
(TypePar const& newValue
),这样我们就可以使用变量和 <int,int,blank>
字面量。
<int,int,blank>int i = 0; returnDevice(i); //Variables
returnDevice(0); //Literals
(C) 在具体例子中,析构函数不需要特殊的实现,但添加 virtual 标识符将有助于我们在以后继承这个类。
现在让我们来处理返回类型 RetPar
。我们希望我们的转换器类在返回端也是透明的!所以我们向 RetPar
数据类型添加一个类型转换运算符。
virtual operator RetPar()
{
return m_value -1; //Just temporary
}
让我们看看它是如何工作的!
//I'm ignoring the FCPar for now
typedef transformationTemplate<int,int,FCPar><int,> tIntConvertor;
const int NUMBER_OF_DEVICES = 90;
class cDevice{}; //A dummy
cDevice arrDevices[NUMBER_OF_DEVICES];
cDevice& returnDevice(tIntConvertor DeviceNumber)
{ //The casting oprator substracts 1 for us
return *(arrDevices[DeviceNumber]);
}
int main()
{
cDevice* tmpDevice= arrDevice[0];
// Do things with the Device
returnDevice(tmpDevice->DeviceID).Work();
return 0;
}
但等等,这还不是全部。我们需要提取转换函数,以便随时更改它。将算法的一部分提取并封装起来的方法称为 Trait。如我之前所说,转换函数应该能够接收一个类型并返回一个新类型
template< class RetPar , class TypePar >
class cTransFuncBinary
{
public:
virtual RetPar operator ()(TypePar value) = 0;
};
这是从现在开始的每个转换类的原型。对于我们的例子,我们将需要这个类
class cTransFuncMinus : public cTransFuncBinary<int>
{
public:
int operator()(int value)
{
return value - 1;
}
};
回到类,让我们将 cTransFuncMinus
类“嵌入”到 transformationTemplate
中。转换应该在转换为 int 的函数中进行。
virtual operator RetPar()
{
FCPar apply;
return apply(m_iData);
}
- 与其在
RetPar
函数中实例化FCPar
,不如持有它的指针成员,并在运行时使用它来更改FCPar
成员。我选择了不这样做。(参见策略设计模式。) - 我选择将其封装为一个类而不是一个函数,因为这样以后更容易扩展。
在添加所有组件后,我们的代码应该看起来像这样
//I'm ignoring the FCPar for now
typedef transformationTemplate< int , int , cTransFuncMinus ><int,> tIntConvertor;
const int NUMBER_OF_DEVICES = 90;
class cDevice{}; //A dummy
cDevice arrDevices[NUMBER_OF_DEVICES];
cDevice& returnDevice(tIntConvertor DeviceNumber)
{ //The casting oprator calls the cTransFuncMinus
return *(arrDevices[DeviceNumber]);
}
int main()
{
cDevice* tmpDevice= arrDevice[0];
// Do things with the Device
returnDevice(tmpDevice->DeviceID).Work();
return 0;
}
源代码
在附件项目中,我玩弄了转换模板的可能性。请随意浏览代码并进行尝试。