ValueList的出现及其应用实践
关键词:MML Typelist ValueList RefHolder TypeHolder
ValueList是一个用来操作一大群值对象的C++工具,就象Loki::Typelist对各种类型提供相同操作一样,ValueList对值对象提供相同操作。实际上ValueList的名字就来源于Typelist,它是在工作实践中总结出来的,是用来解决实际的技术问题,而不仅仅是演示C++模板技巧。
1.1 ValueList的必要性有些时候你必须针对每个值对象编写同样的代码,而且template无法帮上忙。为了说明这个问题,我们来看看ValueList第一次应用需要解决的技术问题。
MML:人机交互语言,一种纯文本格式的协议语言,一般来说其命令格式类似:
<CMD>: <Arg1-Name>=<Arg1-Value>,<Arg2-Name>=<Arg2-Value>[,….];
CMD是命令字,可以简单理解和一个C++中的函数名相当。CMD一般以“:”和后面的参数列表分隔;
Arg1-Name是参数名,可以理解为和C++中的函数声明中的形参相当。参数名一般以“=”和后面的参数值分隔;
Arg1-Value是参数值,可以理解为和C++中函数调用传入的实参相当。参数值一般以“,“和其他参数名-值对分隔
整个MML命令一般以“;“为结束标志。
在笔者的工作环境中,经常需要从外部(标准输入,网络,文件等)读取MML格式的字符串,提取每个参数值的内容,修改内存中此参数对应的变量的值。我们已经定义了MML命令的包装类,用来提取参数名和参数值:
class MMLCommand
{
public:
bool hasArgument(const std::string& arg) const; //是否存在输入的参数名
std::string value(const std::string& arg) const; //提取输入参数名对应的参数值
……
};
现在需要从外部输入的MML命令中获取如下结构的内容:
struct AlmData
{
long devcsn;
long almid;
std::string nename;
…
}
我们定义下面的函数来实现这个功能
void convert(const MMLCommand& cmd, AlmData& alm)
{
if(cmd.hasArgument("devcsn"))
convert_value(cmd.value("devcsn"),alm.devcsn);
if(cmd.hasArgument("alarmid"))
convert_value(cmd.value("alarmid"),alm.alarmid);
if(cmd.hasArgument("nename"))
alm.nename = cmd.value("nename");
……
}
非常遗憾的是,每次使用不同的数据类型来提取MML命令中的参数值我们就要重复编写上面的函数,函数体处理提取每个成员变量的代码也是相似的,但我们没有办法来消除这种重复。
当然我们可以定义这样的模板函数
template<class T1,class T2,class T3>
void getValue(const MMLCommand& cmd, const std::string& arg1, T1& v1, const std::string& arg2,T2& v2, const std:string& arg3,T3);
就可以在程序中通过如下的简单调用来convert函数的重复编写:
getValue(cmd,”devcsn”,alm.devcsn,”alarmid”,alm.alarmid,”nename”,alm.nename);
然而,getValue的模板参数个数是不可能变化的,这就意味做我们还得编写存在不同参数个数的getValue模板函数。
ValueList在结合流的插入提取操作将使得getValue函数真正的泛化成功,可以实现处理任意数量的参数值的功能。当然ValueList将带来更多地收益。
1.2 实现技术下面先抛开MML协议的应用环境,从ValueList的定义和简单应用入手。
1.2.1 ValueList的定义仿照Typelist来定义ValueList
tempate<class T1,class T2> class ValueList
{
public:
typedef T1 first_value_type;
typedef T2 second_value_type;
T1 m_val1;
T2 m_val2;
Valuelist(const T1& v1,const T2& v2):m_val1(v1),m_val2(v2){}
};
这样在使用的时候,只要保证模板参数T2也是一个ValueList类型的,就实现了递归定义可以保存任意数量的一个数据类型。同时和Typelist一样,也需要一个结束类型标志。因此定义下面的结构作为结束标志:
struct null_type{};
同时定义一个便利函数来生成vlauelist,重复调用这个函数可以将内部任意数量(复杂)的数据结构和一个ValueList联系在一起了:
template<class T1,class T2>
ValueList<T1,T2> makeValueList(const T1& v1,const T2& v2)
{
return ValueList<T1,T2>(v1,v2);
}
1.2.2 ValueList的流插入操作符及应用举例首先考虑将ValueList输出到流的实现,非常简单:
template<class char_type, class traits_type, class T1,class T2>
std::basic_ostream<char_type,traits_type>& operator<<(std::basic_ostream<char_type,traits_type>& out, const ValueList<T1,T2>& value)
{
//负责单个变量的输出,如果是基本类型int,floag,const char*等,语言已经支持,如果是自己定义的类型,就需要自己实现了.
out << value.m_va1;
//递归输出其他值
return out << value.m_val2;
}
因为ValueList最后结束于一个null_type,我们需要定义null_type的一个输出操作,否则上面的函数在使用的时候会出现编译错误
template<class char_type,class traits_type>
std::basic_ostream<char_type,traits_type>& operator<<(std::basic_ostream<char_type,traits_type>& out, null_type)
{
//什么都无需做,直接返回即可
return out;
}
应用上面的实现,我们可以输出任意数量(复杂)数据结构的内容。下面举例说明。
struct MyData{
int f1;
long f2;
double f3;
std::string f4
char f5;
}
MyData data;
sd::cout << makeValueList(data.f1, makeValueList(data.f2, makeValueList(data.f3, makeValueList(data.f4, makeValueList(data.f5, null_type() ) ) ) ) );
用这种方式就可以输出任意复杂的数据结构的内容,而不需要写更多的代码。上面makeValueList的嵌套定义可以用宏来替代,如下:
#define VLIST_1(x) makeValueList(x,null_type())
#define VLIST_2(x1,x2) makeValueList(x1,VLIST_1(x2))
#define VLIST_3(x1,x2,x3) makeValueList(x1,VLIST_2(x2,x3))
#define VLIST_4(x1,x2,x3,x4) makeValueList(x1,VLIST_3(x2,x3,x4))
#define VLIST_5(x1,x2,x3,x4,x5) makeValueList(x1,VLIST_4(x2,x3,x4,x5))
…
这样上面最后的输出语句就可以简化为:
std::cout << VLIST_5(data.f1,data.f2,data.f3,data.f4,data.f5);
1.2.3 流的提取操作符带来的复杂性及RefHolder的应用现在考虑从流中提取一个ValueList的每个单元的取值,其实现就要相对复杂一些:
template<class char_type,class traits_type,class T1,class T2>
std::std::basic_istream<char_type,traits_type>& operator>>(std::std::basic_istream<char_type,traits_type>& in, ValueList<T1,T2> data)
{
//首先恢复单个单元的取值值
in >> data.m_val1;
//递归恢复其他单元的取值
in >> data.m_val2;
}
同样的理由,需要对null_type做特殊处理
template<class char_type,class traits_type>
std::std::basic_istream<char_type,traits_type>& operator>>(std::std::basic_istream<char_type,traits_type>& in, null_type)
{
//直接返回即可
return in;
}
请注意上面函数第二个参数的类型, 它为什么不能是const引用? 因为我们需要通过流的提取操作符修改其成员变量的值。 为什么不能是一个引用类型呢? 因为引用类型将给我们带来使用上的不便。还是前一节中的例子,要从标准输入流中提取data变量的值,则我们可以这样写:
std::cin >> VLIST_5(data.f1,data.f2,data.f3,data.f4,data.f5)
因为VLIST_5返回一个临时变量,如果operator>>操作函数的第二个参数是一个非const引用,将导致编译问题. 除非我们能定义一个左值变量,但VLIST_5的返回值类型是:
ValueList<int,ValueList<long,ValueList<double,vlualist<std::string,ValueList<char,null_type>>>>>
这么复杂的一个类型需要使用人员显示定义其一个变量基本上是不可行的,更何况实际应用中还有嵌套更多、更复杂的ValueList,因此我们只能采用值传递的方式来使用ValueList。
接下来我们必须解决值传递方式带来的一个问题。
std::cin >> VLIST_5(data.f1,data.f2,data.f3,data.f4,data.f5) 这个操作根据流中输入的内容修改了一个VLIST_5返回的临时的变量内部的字段,并没有修改内存变量data中的值,而后者才是我们想要的。为了解决这个问题,一个非常自然的想法就是修改vlauelist的定义:
template<class T1,class T2>
class ValueList
{
public:
T1& m_val1;
T2 m_val2;
ValueList(T1& v1, T2 v2): m_val1(v1),m_val2(v2){}
};
如果是这样的定义, std::cin >> VLIST_5(data.f1,data.f2,data.f3,data.f4,data.f5)就可以改变data变量的值,达到了我们的目的。很明显,它要求ValueList的第一个构造参数不能是一个右值(常量,临时变量等),这会在很大程度上限制operator<<的操作,因为这个操作本来是不限制必须是一个左值的。实际应用中,列表的输入输出比较多,仍然以前面定义的MyData为例,考虑下面的使用方法:
std::list<MyData> datalist;
//如果象上面那样修改ValueList的定义,这里编译失败,因为非const引用不能用临时变量来初始化
std::cout << VLIST_1(datalist.size());
除非我们这样使用:
int sz = datalist.size();
std::cout << VLIST_1(sz);
这种使用上的限制显然不能令人满意。因此这样的修改,将带来易用性的问题,不是一个好的解决方案。实际上这里存在一个矛盾, 我们在流的插入操作中仅需要在ValueList中保存一个数据的拷贝或const引用,而在流的提取操作中又需要一个非const引用,为了使用上的方便,又要求他们的行为方式完全一样。这个看起来无法协调的矛盾是可以解决的:就是将一个引用类型包装为一个值类型进行拷贝复制,它出现的主要目的是解决的问题是目前C++不支持引用的引用类型,导致在模板使用中存在一些不便之处,在这里也能用上了。它的实现方式:
template<class T>
class RefHolder
{
T& m_ref;
Public:
RefHolder(T& ref): m_ref(ref){}
Operator T&() const { return m_ref; }
RefHolder& operator=(const T& v) { m_ref = v; }
};
对于任何一种模板类的定义都可以定义一个对应的模板函数用于方便生成这个模板类的一个实例,我们定义下面的便利函数:
template<class T>
RefHolder<T> ByRef(T& ref)
{
return RefHolder<T>(ref);
}
利用这个RefHolder<T>的定义,我们定义一个makeValueList的重载函数:
template<class T1,class T2>
ValueList<RefHolder<T1>, T2> makeValueList( T1& ref, const T2& t2)
{
return ValueList<RefHolder<T1>,T2>(ByRef(ref),t2);
}
利用重载函数的解析,让编译器根据输入的实参类型自动选择是否使用RefHolder(其实最好的一个办法,是显示推导出给定类型对应的const引用和非const引用类型,根据这两个类型定义参数,类似: makeValueList< TypeTraits<T1>::reference, ….) , makeValueList< TypeTraits<T1>::const_reference, ….)这里又需要引入各种相关类型的推导模板类的定义,相对复杂,但更精确,TypeTraits的定义在Loki库中有实现,可以参考),这样我们的代码就不需要任何改变了,当然,需要定义RefHolder的流插入提取操作:
template<class char_type,class traits_type,class T>
std::std::basic_ostream<char_type,traits_type>& operator<<(std::std::basic_ostream<char_type,traits_type>& out, const RefHolder<T>& data)
{
return out << (T&)data;
}
template<class char_type,class traits_type,class T>
std::std::basic_istream<char_type,traits_type>& operator>>(std::std::basic_istream<char_type,traits_type>& in, RefHolder<T> data)
{
return in >> (T&)data;
}
1.2.4 ValueList的完整定义及使用template<class T>
class RefHolder
{
T& m_ref;
Public:
RefHolder(T& ref): m_ref(ref){}
Operator T&() const { return m_ref; }
RefHolder& operator=(const T& v) { m_ref = v; }
};
template<class T>
RefHolder<T> ByRef(T& ref)
{
return RefHolder<T>(ref);
}
template<class char_type,class traits_type,class T>
std::std::basic_ostream<char_type,traits_type>& operator<<(std::std::basic_ostream<char_type,traits_type>& out, const RefHolder<T>& data)
{
return out << (T&)data;
}
template<class char_type,class traits_type,class T>
std::std::basic_istream<char_type,traits_type>& operator>>(std::std::basic_istream<char_type,traits_type>& in, RefHolder<T> data)
{
return in >> (T&)data;
}
class null_type{};
tempate<class T1,class T2> class ValueList
{
public:
T1 m_val1;
T2 m_val2;
Valuelist(const T1& v1,const T2& v2):m_val1(v1),m_val2(v2){}
}
template<class T1,class T2>
ValueList<T1,T2> makeValueList(const T1& v1,const T2& v2)
{
return ValueList<T1,T2>(v1,v2);
}
template<class T1,class T2>
ValueList<RefHolder<T1>, T2> makeValueList( T1& ref, const T2& t2)
{
return ValueList<RefHolder<T1>,T2>(ByRef(ref),t2);
}
template<class char_type, class traits_type, class T1,class T2>
std::basic_ostream<char_type,traits_type>& operator<<(std::basic_ostream<char_type,traits_type>& out, const ValueList<T1,T2>& value)
{
out << value.m_va1;
return out << value.m_val2;
}
template<class char_type,class traits_type>
std::basic_ostream<char_type,traits_type>& operator<<(std::basic_ostream<char_type,traits_type>& out, null_type)
{
return out;
}
template<class char_type,class traits_type,class T1,class T2>
std::std::basic_istream<char_type,traits_type>& operator>>(std::std::basic_istream<char_type,traits_type>& in, ValueList<T1,T2> data)
{
in >> data.m_val1;
in >> data.m_val2;
}
template<class char_type,class traits_type>
std::std::basic_istream<char_type,traits_type>& operator>>(std::std::basic_istream<char_type,traits_type>& in, null_type)
{
return in;
}
1.2.5 MML命令格式的应用现在我们需要利用已有的基础来实现MML命令的解析和组装功能。实际我们只需要将MML命令中” arg_name = arg_value”格式的字符串转换为一个内部变量的值。 这样也就需要我们给每个内部变量取一个名字,它要和MML命令中的arg_name一一对应。因此定义一个NameValue。
template<class T>
struct NameValue
{
const std::string m_name;
T m_value;
NameValue(const std::string& n, T v):m_name(n),m_value(v){}
};
同样定义其便利函数, 同样的原理,利用函数重载功能自动选择是否使用RefHolder:
template<class T >
NameValue<T> makeNameValue(const std::string& name, const T& v)
{
return NameValue<T>(name,v);
}
template<class T>
NameValue<RefHolder<T> > makeNameValue(const std::string& name, T& v)
{
return NameValue<RefHolder<T> >(name,ByRef(v));
}
然后再实现它的流操作函数即可:
template<class char_type,class traits_type,class T>
std::std::basic_ostream<char_type,traits_type>& operator<<(std::std::basic_ostream<char_type,traits_type>& out, const NameValue<T>& data)
{
return out << data.m_name << “=” << data.m_value << “,”;
}
template<class T>
std::std::basic_istream<char_type,traits_type>& operator>>(std::std::basic_istream<char_type,traits_type>& in, NameValue<T> data)
{
//根据MML格式, 利用”=”和”,”为分界符号,提取name和value值
std::string name;
……
std::string value;
……
if(name == data.m_name)
{
std::istringstream str(value);
//又回到基本类型从流中的提取操作符了
str >> data.m_value;
}
return in;
}
最后我们需要一个方便生成NameValue的一个宏,类似VLIST_xxx的定义:
#define NVLIST_1 (n, v) makeValueList(MakeNameValue(n,v),null_type())
#define NVLIST_2(n1,v1,n2,v2) makeValueList(MakeNameValue(n1,v1),NVLIST_1(n2,v2))
….
仍然利用前面MyData的例子,做下面的定义:
#define MyData_MML(x) NVLIST_5(“int”,x.f1,”long”,x.f2,”double”,x.f3”,”string”,x.f4,”char”,x.f5)
std::cout << MyData_MML(data);
std::cin >> MyData_MML(data);
实际上以后我们对于任何类型的应用都类似这三行代码,利用NVLIST_xx来定义一个被使用类型专用的宏, 然后直接使用即可.
1.2.6 进一步考虑MML参数位置无关性的处理实际应用中的MML命令中的参数顺序是无关紧要的。前面的实现不能支持这个特性,为了解决参数顺序无关性的问题,需要重新考虑MML的解析过程。
同样利用ValueList的递归性质来完成: 如果目前解析的MML参数和当前单元的名字不匹配,则试图匹配下一个单元。
首先读取单个NameValue值,返回值表示输入和实际NameValue是否匹配.
template<class T>
bool getValue(const std::string& name,std::string& value, NameValue<T> data)
{
if( name != data.m_name) return false;
std::istringstream str(value);
str >> data.m_value;
return true;
}
根据提取的参数名,递归寻找ValueList<T1,T2>中的匹配项,如果没有任何匹配项则可能和null_type打交到,因此也要定义一个特殊的函数
template<class T1,class T2>
bool getValue(const std::string& name, const std::string& value, ValueList<NameValue<T1>, T2> data)
{
if(!getValue(name,value,data.m_val1) return getValue(name,value,data.m_val2);
return true;
}
bool getValue(const std::string& name, const std::string& value,null_type)
{
return false;
}
然后从外部流中逐过读取每个MML格式的参数,将得到的参数名和参数值利用上面的函数进行匹配. 当然这里假设了ValueList中所有值都是NameValue类型的, 如果不是将在编译阶段报告错误。
template<class char_type,class traits_type,class T1,class T2>
std::std::basic_istream<char_type,traits_type>& operator>>(std::std::basic_istream<char_type,traits_type>& in, ValueList<NameValue<T1>,T2> data)
{
while(in.rdbuf()->in_avail())
{
//根据MML格式, 利用”=”和”,”为分界符号,提取name和value值
std::string name;
…..//获取name
std::string value;
….//获取vlaue
getValue(name,value,data);
}
}
上面的样例代码还不完善,将流输入结束作为循环结束标识不是太合理, 可以考虑读取MML命令的结束标识”;”或者只要任何一个MML参数和ValueList中的值不匹配就返回,或者固定获取ValueList的长度一样多的参数进行比较。对于获取ValueList长度的方式,Typelist的实现方式可以参考,可以说是完全一样,直接拿来应用即可。这些方式要实现都很容易,这里就不详细列出了。
1.2.7 其他细节带来的问题在实际应用中,还遇到char类型的特殊处理. 程序内部使用char类型一般都是同其他整型同样的使用,只是取值范围不同而已,然而将char类型从流中执行插入提取操作的结果可能和我们想象的相差比较大:
char c = 10;
std::cout << c;
我们希望最后的输出就是一个字符串”10”,但实际结果确是一个不可见字符.因此对于char类型需要特殊处理。由此引申开,实际应用中,很可能需要将一种类型看做另外一种类型进行各种操作,需要我们实现类型替换.
同样利用模板来实现:
template<class T1,class T2>
class TypeHolder
{
T2 m_t2;
Public:
TypeHolder(const T2& t2): m_t2(t2){}
operator const T1&(){ return (T1)m_t2; }
TypeHolder& operator=(const T1& v) { m_t2 = v; }
}
和前面一样,定义便利函数,并利用函数重载机制自动选择是否使用RefHolder
template<class T1,class T2>
inline TypeHolder<T1,T2> ByType(const T2& t)
{
return TypeHolder<T1,T2>(t);
}
template<class T1,class T2>
inline TypeHolder<T1,RefHolder<T2> > ByType(T2& t)
{
return TypeHolder<T1,RefHolder<T2> >(ByRef(t));
}
同样的,只要定义了它的输入输出流操作符,就可以直接使用了
template<class char_type,class traits_type, class T1,class T2>
std::basic_ostream<char_type,traits_type>& operator<<(std::basic_ostream<char_type,traits_type>& out, const TypeHolder<T1,T2>& data)
{
return out << (T1)(data);
}
template<class char_type,class traits_type, class T1,class T2>
std::basic_istream<char_type,traits_type>& operator<<(std::basic_istream<char_type,traits_type>& in, TypeHolder<T1,T2> data)
{
T1 tmp;
in >> tmp;
data = tmp;
return in;
}
上面的实现过于简单,主要体现在类型之间的转换,很可能有些类型之间无法直接转换,比较好的方式是将转换动作也定义为TypeHolder的模板参数,从外界传入,当然我们提供一个合理的缺省实现即可。完善的实现方式与这里的主题无关,就不在这儿详细说明。
另外,std::string的提取也比较特殊,缺省情况下,如果输入流中的字符串中间存在空格,提取操作将导致字符串被截断。这次我们采用特化getValue函数的方法来解决:
template<>
bool getValue<std::string>(const std::string& name,std::string& value, NameValue<std::string> data)
{
if( name != data.m_name) return false;
data.m_value = value;
return true;
}
最后完善前面的例子,我们需要将MyData中的char字段看作int使用,重新定义:
#define MyData_MML(x) NVLIST_5(“int”,data.f1,”long”,data.f2,”double”,data.f3,”string”,data.f4,”char”,ByType<int>(data.f5))
std::cout << MyData_MML(data);
std::cout >> MyData_MML(data);
到现在为止,我们已经完全可以共享MML编解码和内存数据自动映射的实现代码. 开篇看到的简单重复的代码可以用用上面的三行代码来完全替代了.
1.2.8 最后的总结结合流的插入提取操作的ValueList可以大大简化从MML命令取值以及组装MML命令的过程.其实这种思想还可以非常方便的应用于其他格式的解析,比如应用程序外部输入参数的解析,甚至ASN.1编解码,采用类似的手段,也可以很方便的实现。ValueList当然还有更广泛的用途,当它和STL库中的容器结合在一起的时候,我们可以使用功能更强大的标准容器。
本文地址:http://com.8s8s.com/it/it27129.htm