假设我们现在有个任务,要做一个程序,将一个数组中的每一个元素乘上100,然后赋值回去。为此,我写下了这样的C#代码:
int[] ai=new int[10];
//初始化ai。
foreach(int i in ai)
{
i*=100;
}
我又写了C++代码:
vector<int>
ai(10);
//初始化ai。
for(vector<int>::iterator i=ai.begin(); i!=ai.end(); ++i)
{
(*i)*=100;
}
唔,代码比C#长多了。C++真复杂。这个或许好点:
…
for(int i=0; i<ai.size(); ++i)
{
ai[i]*=100;
}
现在任务升级了,有三个数组,把一个数组的元素乘上第二个数组相应元素,放入第三个数组中。我想写这样的代码(C#):
int[] ai1=new int[10],ai2=new int[10], ai3=new int[10];
//初始化ai1和ai2
foreach(int i in ai1, int j in ai2, int k in ai3)
//语法错误, 要能这么写
{
// 多好,可惜
i=j*k;
}
没有这种语法,得用for:
…
for(int i=0; i<0; ++i)
{
ai1[i]=ai2[i]*ai3[i];
}
C++版的是这样:
vector<int>
ai1(10), ai2(10), ai3(10);
//初始化ai1和ai2
for(int i=0; i<0; ++i)
{
ai1[i]=ai2[i]*ai3[i];
}
代码一样。但是,C++有个transform算法,可以不用循环:
…
int mul_them(int v,int u) {
return v*u;
}
transform(ai1.begin(), ai1.end(), ai2.begin(), ai3.begin(), mul_them);
代码没有简单,可晦涩多了。不熟悉标准库的初学者,会觉得这像是天书。我们一会儿来讨论学习问题。
再加点难度,三个容器,第一个是队列,第二个是数组,第三个是链表。把队列里的元素乘上数组里的元素,放到链表里。C#队列和链表没有随机访问操作符[],必须利用IEnumerable<>泛型接口访问:
Queue<int> ai1=new stack<int>(10);
int[] ai2=new int[10]
LinkedList<int> ai3=new queue<int>(10);
//初始化ai1和ai2
IEnumerator<int> eai1=ai1.GetEnumerator();
IEnumerator<int> eai2=ai2.GetEnumerator();
IEnumerator<int> eai3=ai3.GetEnumerator();
while(eai1.MoveNext() && eai2.MoveNext() && eai3.MoveNext())
{
eai3.Current=eai1.Current*eai2.Current;
}
下面该C++代码了。C++标准库的队列和链表也没有随即操作符[],必须使用迭代器:
deque<int>
ai1(10);
vector<int> ai2(10);
list<int> ai3(10);
//初始化ai1和ai2
int mul_them(int v,int u) {
return v*u;
}
transform(ai1.begin(), ai1.end(), ai2.begin(), ai3.begin(), mul_them);
C++也可以用手工循环访问迭代器,就像C#那样。当然,代码会复杂得多,比C#的还复杂。
再提高难度,我发现这种运算在很多地方出现,想把它做成一个算法。但是,每次出现的三种容器的类型不一定,元素的类型也不同,我必须作一个泛型算法。先做C#的版本:
public class MyAlogrithm
{
public static void Caculate_Contain<C1,T1,C2,T2,C3,T3>(C1 c1, C2 c2, C3 c3)
where C1: IEnumerator<T1>
where C2: IEnumerator<T2>
where C3: IEnumerator<T3>
{
IEnumerator<T1> eai1 = c1.GetEnumerator();
IEnumerator<T2> eai2 = c2.GetEnumerator();
IEnumerator<T3> eai3 = c3.GetEnumerator();
while (eai1.MoveNext() && eai2.MoveNext() && eai3.MoveNext())
{
eai3.Current = eai1.Current * eai2.Current;//编译错误
}
}
}
有编译错误。eai1.Current和eai2.Current分别返回T1和T2类型的实例,因为C#编译器不知道T1和T2的类型,所以不能确定两者是否能够相乘。此路走不通,我正在努力解决这个问题。剩下一种办法,就只能采用RTTI(RunTime Type Info)解决:
public class MyAlogrithm
{
public static void Caculate_Contain<C1,C2,C3>(C1 c1, C2 c2, C3 c3)
where C1 : IEnumerable
where C2 : IEnumerable
where C3 : IEnumerable
{
IEnumerator eai1 = c1.GetEnumerator();
IEnumerator eai2 = c2.GetEnumerator();
IEnumerator eai3 = c3.GetEnumerator();
while (eai1.MoveNext() && eai2.MoveNext() && eai3.MoveNext())
{
if(eai3.Current.type ==typeof(int)
&& eai1.Current.type ==typeof(int)
&& eai2.Current.type ==typeof(int))
{
(int)eai3.Current = (int)eai1.Current * (int)eai2.Current;
}
else if(eai3.Current.type==typeof(float)
&& eai1.Current.type==typeof(int)
&& eai2.Current.type ==typeof(int))
{
(float)eai3.Current = (int)eai1.Current * (int)eai2.Current;
}
else if(eai3.Current.type ==typeof(double)
&& eai1.Current.type ==typeof(float)
&& eai2.Current.type ==typeof(int))
{
(double)eai3.Current = (float)eai1.Current
* (int)eai2.Current;
}
…
}
}
}
如果不喜欢大量的if…else…,那么可以用Dictionary做一个分派器,根据容器元素的类型分派操作。这种做法有个限制性要求:必须事先约定容器的元素类型,比如int、long、double等等,可以是用户定义的类型(重载过*操作符)。代码的缺点么,都应该看到了:如果约定的容器元素的类型有n个,那么if…else…的数量将是n的立方。
回过头来在看C++的实现:
template<typename T1, typename T2, typename R>
R mul_them(T1 v,T2 u) {
return v*u;
}
template<typename C1, typename C2, typename C3>
Caculate_Container(const C1& c1, const C2& c2, C3& c3)
{
transform(c1.begin(), c1.end(), c2.begin(), c3.begin(),
mul_them<C1::value_type, C2::value_type, C3::value_type>);
}
就这么简单。由于模板是编译期的机制,所有未确定类型(模板参数),是否包含所需的成员,如begin()、end()等,以及是否能够相乘,编译时便会立刻明了。而C#的泛型(确切地说,是.net的泛型),是跨语言的机制,泛型算法可能会被其他语言使用,无法在编译期确定类型的特征,所以必须要求类型在编译前明确所应具备的条件,即类型约束(where字句)。但问题还不在这里。问题在于,C#的泛型的类型约束依赖于继承,即类型必须从…类处继承而来。而不是像C++09中的concept那样直接描述类型所需的特征。所以,C#版的泛型算法也就是因此而失败。
最后再做一个小小的扩展,要求Caculate_Catainer<>()算法可以接受一个算法,该算法指示了如何对这些容器进行操作。此时,我们会发现,C#的泛型算法反而能够实现了:
public delegate void alg<T1, T2, R>(T1 v1, T2 v2, R r);
public static void Caculate_Contain<C1, T1, C2, T2, C3, T3>
(C1 c1, C2 c2, C3 c3, alg<T1, T2, T3> a )
where C1: IEnumerable<T1>
where C2 : IEnumerable<T2>
where C3 : IEnumerable<T3>
{
IEnumerator<T1> eai1 = c1.GetEnumerator();
IEnumerator<T2> eai2 = c2.GetEnumerator();
IEnumerator<T3> eai3 = c3.GetEnumerator();
while (eai1.MoveNext() && eai2.MoveNext() && eai3.MoveNext())
{
a(eai1.Current, eai2.Current,eai3.Current);
}
}
//使用
public static void CaculThem(int v1, int v2,int r) {
r=v1*v2;
}
Caculate_Contain(ai1, ai2, ai3, new alg<int, int, int>(CaculThem));
public static void CaculThem2(float v1, int v2,double r) {
r=v1*v2;
}
Caculate_Contain(af1, ai2, ad3, new alg<float, int, double>(CaculThem2));
我使用了一个委托,作为传递处理容器元素的算法的载体。使用时,用具体的算法创建委托的实例。但具体的算法CaculThem()必须同相应的容器元素类型一致。
下面轮到C++:
template<typename C1, typename C2, typename C3, typename Alg>
Caculate_Container(const C1& c1, const C2& c2, C3& c3, Alg a)
{
transform(c1.begin(), c1.end(), c2.begin(), c3.begin(), a);
}
//使用
template<typename T1, typename T2, typename R>
R mul_them(T1 v,T2 u) {
return v*u;
}
Caculate_Container(ai1, ai2, ai3, mul_them<int, int, int>);
Caculate_Container(af1, ad2, ad3, mul_them<float, double, double>);
如果容器元素有所变化,C#代码必须重写算法CaculThem()。但C++不需要,由于mul_them<>()本身是个函数模板,那么只需将这个函数模板用新的类型实例化一下即可。
C++的代码相对简单些,灵活性也更高些。但这还不是全部,C++还有一个最终极的解法,不需要循环,不需要创建模板算法:
transform(c1.begin(), c1.end(), c2.begin(), c3.begin(), _1*_2);
没看明白?我一开始也看不明白。这里用到了boost库的Lambda表达式。_1占位符对应c1的元素,_2的占位符对应c2的元素,_1*_2表示才c1的元素乘上c2的元素,其结果放在c3里。表达式可以写得更复杂,比如(_1*_2+3*_1)/(_1-_2)。Lambda表达式可以用在所有需要操作的算法中,比如我要去掉字符串中的“-”,可以这样写:
remove_if(s.begin(), s.end(), _1==’-’);
Lambda
表达式基于一种叫做“模板表达式”的技术,通过操作符重载,将一个表达式一层一层地展开,构成一个解析树。然后作为一个函数对象传递给算法,算法在循环内调用函数对象,执行相应的计算。