Step By Step(C++模板Trait)
Trait是C++模板设计的一个基本应用技巧,通过应用Trait模板参数,可以在工业强度的程度设计中既保证了程序的灵活性,同时也有效的减少了类型参数的数量。对于普通函数而言,如果想要对该函数的功能进行更为细粒化的控制,一个主要的方法便是为该函数增加更多的参数,而函数体内的逻辑代码则会根据参数值的不同而选择不同的逻辑分支,最终返回不同的结果。一个极端的现象是函数的参数过多,而且并不是每个参数都会为每个调用者所用,如果简单的使用缺省值,那么这些缺省参数则必须位于函数参数的末尾。如果有朝一日需要新增函数参数,而该参数恰恰又没有缺省值,此时我们只能将该新增的参数放到所有缺省参数的前面,一旦如此,已有的函数调用者都将不得不进行相应的修改以适应该函数签名的改变。同时随着参数的增多,也会给调用者带来额外的负担。一个更好的方法是将函数参数改为结构体的字段,而函数的参数也调整为该结构体的指针或者引用。相比于此,Trait模板类可以被视为刚刚提到的结构体,但是不同的是该Trait类的行为需要依赖另外一个模板参数,既当前类的主模板参数。为了更好的理解Trait在模板中的应用场景,我们先给出一个简单的示例,并再一步一步将其改造为对Trait的应用,我想这样将更加便于大家的理解。
这里是一个非常简单的例子,但是却非常适合Trait的学习。该示例函数的主要功能是遍历模板参数类型的数组,并逐个累加每一个元素同时返回最后的累加值。
1 #include <stdio.h> 2 3 template<typename T> 4 T accumulate(T const* begin, T const* end) { 5 T total = T(); 6 while (begin != end) { 7 total += *begin; 8 ++begin; 9 } 10 return total; 11 } 12 13 int main() { 14 int test[5] = {1,2,3,4,5}; 15 char chartest[] = "templates"; 16 int r = accumulate(test,test + 5); 17 int r2 = accumulate(chartest, chartest + sizeof(chartest)); 18 printf("r is %d\n",r); 19 printf("r2 is %d\n",r2); 20 return 0; 21 } 22 //r is 15 23 //r2 is -49
对于上例的输出结果,r2的值可能并不是我们想要的,那么为什么会出现这样的结果呢?很简单,在第二次调用模板函数accumulate的时候,模板参数被推演成char,而char的最大值为127,因此total的结果一旦超过该值就会变为负数。为了解决该问题,我们将不得不引入更多的模板参数作为函数的返回值类型,而返回值是不会参与类型推演的,所以在使用时就必须显示指定该模板参数的实际类型。当然,我们也可以通过为主模板参数T引入一个与其关联的trait模板类来解决该问题。见如下代码和关键性注释。
1 #include <stdio.h> 2 3 template<typename T> 4 class AccumulationTraits; //该类为trait模板类的基础模板类,由于不会真实的使用,因此不需要定义。 5 6 //下面是针对每一个数值型的原始类型都特化了一个Trait模板类,这样编译器在实例化accumulate函数时 7 //会根据其类型参数的不同选择不同的特化Trait类。 8 template<> 9 class AccumulationTraits<char> { 10 public: 11 typedef int AccT; 12 }; 13 14 template<> 15 class AccumulationTraits<short> { 16 public: 17 typedef int AccT; 18 }; 19 20 template<> 21 class AccumulationTraits<int> { 22 public: 23 typedef long long AccT; 24 }; 25 26 template<> 27 class AccumulationTraits<unsigned int> { 28 public: 29 typedef unsigned long long AccT; 30 }; 31 32 template<> 33 class AccumulationTraits<float> { 34 public: 35 typedef double AccT; 36 }; 37 38 //这里将该函数的返回值类型改为主模板参数T的trait类的指定属性。 39 template<typename T> 40 typename AccumulationTraits<T>::AccT accumulate(T const* begin, T const* end) { 41 typedef typename AccumulationTraits<T>::AccT AccT; 42 AccT total = AccT(); 43 while (begin != end) { 44 total += *begin; 45 ++begin; 46 } 47 return total; 48 } 49 50 int main() { 51 int test[5] = {1,2,3,4,5}; 52 char chartest[] = "templates"; 53 int r = accumulate(test,test + 5); 54 int r2 = accumulate(chartest, chartest + sizeof(chartest)); 55 printf("r is %d\n",r); 56 printf("r2 is %d\n",r2); 57 return 0; 58 } 59 //r is 15 60 //r2 is 975
尽管通过Trait解决了返回值越界的问题,但是上面的代码示例仍然存在一些问题,如:AccT total = AccT(),这行代码要求AccT类型必须是原始类型,或者是有缺省构造的类类型,否则将会导致编译错误。在这里,我们可以进一步升级Trait类,以便accumulate函数能够通过Trait的一个属性获得total的缺省初始值。如:
1 #include <stdio.h> 2 3 template<typename T> 4 class AccumulationTraits; 5 6 //相比于上面的示例,该示例为每一个trait的特化类都新增了一个属性,即zero()函数,用来表示 7 //AccT类型的缺省初始值。这里之所以用zero函数,而不是普通的常量值,是因为C++中要求const 8 //限定符修饰的成员变量只能是整数类型或枚举类型,而不能是浮点或其他自定义类型。 9 template<> 10 class AccumulationTraits<char> { 11 public: 12 typedef int AccT; 13 static AccT zero() { return 0; } 14 }; 15 16 template<> 17 class AccumulationTraits<short> { 18 public: 19 typedef int AccT; 20 static AccT zero() { return 0; } 21 }; 22 23 template<> 24 class AccumulationTraits<int> { 25 public: 26 typedef long long AccT; 27 static AccT zero() { return 0; } 28 }; 29 30 template<> 31 class AccumulationTraits<unsigned int> { 32 public: 33 typedef unsigned long long AccT; 34 static AccT zero() { return 0; } 35 }; 36 37 template<> 38 class AccumulationTraits<float> { 39 public: 40 typedef double AccT; 41 static AccT zero() { return 0; } 42 }; 43 44 template<typename T> 45 typename AccumulationTraits<T>::AccT accumulate(T const* begin, T const* end) { 46 typedef typename AccumulationTraits<T>::AccT AccT; 47 AccT total = AccumulationTraits<T>::zero(); 48 while (begin != end) { 49 total += *begin; 50 ++begin; 51 } 52 return total; 53 }
相比于之前的代码,上述示例尽管已经有了很大的改进,但是仍然存在一个明确的缺陷。在有些情况下,我们希望accumulate函数能够使用我们自定义的Trait类,而不是之前已经定义的一组AccumulationTrait模板类,为了解决这一问题,可以考虑为该函数增加一个模板参数,即Trait的类型,同时也为该模板参数提供缺省值,即AccumulationTrait。然而事与愿违,C++的标准并不允许模板函数包含缺省模板参数,因此我们不得不进行必要的变通,即将accumulate函数改为模板类的静态成员函数,如:
1 template<typename T, typename AT = AccumulationTraits<T> > 2 class Accum { 3 public: 4 static typename AT::AccT accumulate(T const* begin, T const* end) { 5 typename AT::AccT total = AT::zero(); 6 while (begin != end) { 7 total += *begin; 8 ++begin; 9 } 10 return total; 11 } 12 }; 13 //由于模板类在实例化的过程中无法根据参数进行类型推演,因此这里我们提供了一个模板 14 //函数便于调用者的使用,又因为在大多数情况下我们都会使用缺省trait,只有在个别的 15 //情况下才会考虑使用自定义的trait类,因此我们提供的第一个辅助函数将仅包含一个模板参数。 16 template<typename T> 17 typename AccumulationTrait<T>::AccT accumulate(T const* begin, T const* end) { 18 return Accum<T>::accumulate(begin,end); 19 } 20 //这里提供的第二个辅助函数,将允许调用者使用自定义的Trait类,这里之所以将T作为第二个 21 //参数,是因为该类型可以通过函数的参数推演出来,因此调用者在使用时可以不用显示指定该 22 //模板参数的实际类型,而是直接通过函数参数进行推演即可。凡是使用该辅助函数的调用者, 23 //都是希望提供自定义的Trait类,否则就会直接调用上一个辅助函数。 24 template<typename Traits,typename T> 25 typename Traits::AccT accumulate(T const* begin, T const* end) { 26 return Accum<T, Traits>::accum(begin,end); 27 }