泛型程序设计之 Trait 与 Policy 技巧初探

来源:互联网 发布:制作幻灯片的软件 编辑:程序博客网 时间:2024/05/22 12:18

  • 引言
  • 什么是 trait
  • 思维跳板
  • 一个例子
  • 分析一下
  • 什么是 policy
  • 进阶例子
  • 分析一下
  • 总结一下


引言

以下内容涉及 C++ 模板使用C++ 编译期概念C++ 类型体系泛型程序设计思想C++ 元编程思想坑深慎入,作者也并非精通,只是有感而谈。

内容 项 C++模板使用 函数模板 类模板 模板参数类型 函数模板参数自动推导 模板参数缺省值 模板的模板参数 模板参数实例化 模板特化与重载 C++类型体系 基本类型 自定义类型 添加修饰符的类型 泛型程序设计思想 什么是泛型 一个简单的实例 什么是代码语义 从语义层优化代码设计 C++编译期概念 编译期数值计算 编译期类型计算 编译期静态数据 C++ 元编程思想 跨越编译期与运行期界限 什么是元数据 什么是元函数 什么是元函数转发

前方高能

什么是 trait?

trait 是一种 C++ 程序设计机制,它有助于对某些额外参数的管理,这里的额外参数是指:在具有工业强度的模板设计中所出现的参数。——《C++ Templates》Chapter.15

traits n…(noun.) 用来刻画一个事物的(与众不同的)特性。——《New Shorter Oxford English Dictionary》

trait class 是一种用于代替模板参数的类。作为一个类,它可以是有用的类型,也可以是常量;作为一个模板,它提供了一种实现“额外层次间接性”的途径,而正是这种“额外层次间接性”解决了所有的软件问题。—— Nathan Myers

思维跳板

还记得之前探究过类型萃取的问题,那就是 trait 技巧的雏形。

定义一个提取类型的模板:

template <typename T>struct MyTrait {    typedef T type;};// 特化成不同种的形式用以萃取某种类型的信息

一个例子

还是使用四则运算来距离吧,一个加法运算:

template <typename T>T sum(T value1, T value2) {    T result = T();    return value1+value2;}

我们开始使用这个东西:

cout << sum(5,6) << endl; // 没问题cout << sum(2.5,2.5) << endl; // 没问题cout << sum(2.5,5) << endl; // 类型冲突,需要显式声明模板类型cout << sum(5,2.5) << endl; // 类型冲突,需要显式声明模板类型

OK,站在这个模板的开发者的角度来看,我们需要这个模板能够计算任何支持加法计算概念的类型的值。
这里出现一个问题,当使用了不同的类型的时候,虽然直观上它们是不同的类型,但是它们可以通过转换之后再计算,这就是考验真功夫的时候了。

对于内置数值类型,我们可以考虑以下措施:
- 低精度向高精度转换(int -> float -> double)
- 不可计算类型向可计算类型转换(自定义复数类型 -> int -> float -> double)

这样子,对于之前代码中的问题,就转化成了低精度向高精度转换,转换的步骤如下:
- 获取类型
- 类型比较
- 选择类型

这里转换要考虑一个位数的问题,即低位数向高位数转换,低精度向高精度转换。考虑到在64位机上 C++ 的各种类型的长度:

类型 长度 short 2 int 4 long 8 long long 8 float 4 double 8 long double 16

提供以下特化模板:
short -> int | long | long long | float | double | long double
int -> long | long long | float | double | long double
long -> long long | double | long double
long long -> double | long double
float -> double | long double
double -> long double

然后我们定义模板:

// 主模板,默认第一个参数的精度大于等于第二个template <typename T1, typename T2>struct convert {    typedef T1 value;};// 我们特化的情况都是第二个参数的精度大于第一个// short converttemplate <>struct convert<short, int> {    typedef int value;};// 以下省略...

定义主函数,无法直接定义,错误原因未知,猜测是无法正确推导模板实参(捂脸)

template <typename T1, typename T2, typename RT = typename convert<T1, T2>::value>RT sum(RT value1, RT value2) {    return value1 + value2;}

所以需要引入一个辅助元函数,在编译期为我们计算出正确的类型

// assistant functortemplate <typename T1, typename T2>struct _sum {    typedef typename convert<T1, T2>::value RT;    RT operator() (RT value1, RT value2) {        return value1+value2;    }};template <typename T1, typename T2>typename _sum<T1, T2>::RT sum(T1 value1, T2 value2) {    return _sum<T1, T2>()(value1, value2);;}

测试函数也就不发了,都知道怎么玩,基本就是 sum(a, b) 的形式,无需指定模板参数类型。

另外,内置类型的各种添加修饰符版本,可以通过萃取直接得到根本的类型,然后进行计算。

返回值返回成左右值什么的看个人情况添加,在 STL type_traits 库里面应有尽有。

分析一下

在上面的例子中,所有的低精度类型向高精度类型、低位数类型向高位数类型转换的特化模板的集合,就是一个 trait 模板,即 convert 集合被称为一个 trait 模板,convert 模板集合存储了所有与加法运算关联的类型。又比如,我希望两个 char 类型的值相加并取平均值,其结果应该是一个位于0~127之间的值。

int main(int argc, char* argv[]){    char a = 'a';    char c = 'c';    char s = sum(a, c)/2;    cout << s << endl; // 期望结果是 'b',实际结果是 '?'    cout << (int)s << endl; // 期望结果是98,实际结果是-30    return 0;}

为什么,因为 char 类型的范围只有0~127,超过128会越界,那么我们还是跟精度转换的问题一样,通过添加一个额外的特化模板来描述 char 类型该如何转换。

template <>struct convert<char, char> {    typedef int value;};

这样子结果就是我们所期望的了。

如果有新的类型需要使用 sum 模板,那么只需要在 convert 模板集中添加一个新的特化模板用来关联新的类型即可。

这里注意到 sum 模板了吗,sum 模板在做类型选择的时候实际上是引入了一个额外的模板参数 RT 作为类型计算的返回值。

template <typename T1, typename T2, typename RT = typename convert<T1, T2>::value>RT sum(RT value1, RT value2) {    return value1 + value2;}

于是为了计算这个 RT,又添加了一个辅助计算的元函数,并且把计算功能也移到了这个辅助元函数里面去,而原本的函数模板只起到了传递模板类型参数和值参数的作用。(有没有跨越编译期和运行期界限的感觉?)

template <typename T1, typename T2>struct _sum {    typedef typename convert<T1, T2>::value RT;    RT operator() (RT value1, RT value2) {        return value1+value2;    }};

这里的 RT 是 convert 模板集经过筛选(计算)之后得出的结果,也是额外参数与 trait 模板的一个关联点

到这里,trait 技巧就初步探索完毕了,一句话总结:trait 技巧就是用模板特化额外的模板参数将一些算法与不同的类型相关联,使之能够公用。

什么是 policy?

一个 policy 类就是一个提供了一个接口的类,该接口能够在算法中应用一个或多个 policy。——《C++ Templates》Chapter.15

policy n…(noun.) 为了某种有益或有利的目的而采取的一系列动作。——《New Shorter Oxford English Dictionary》

policy 表述了泛型函数和泛型类的一些可配置行为(通常都具有被经常使用的缺省值)。——《C++ Templates》Chapter.15

进阶例子

加法只是示例的一个行为,实际中可能有很多不同的操作,比如:减法(捂脸)

那么如何将这些不同的操作,同样配置到这个函数中?

我们需要一个思维上的转变,即被操作对象与操作方法都是可以通过参数化配置的,我们写出了如下代码:

// assistant functortemplate <typename T1, typename T2>struct _sub {    typedef typename convert<T1, T2>::value RT;    RT operator() (RT value1, RT value2) {        return value1-value2;    }};template <typename T1, typename T2>typename _sub<T1, T2>::RT sum(T1 value1, T2 value2) {    return _sub<T1, T2>()(value1, value2);;}

其中新的辅助元函数作为新的操作方法,然后在原始的模板函数中使用这个方法即可,但是这里会有一个 call to function is ambiguous 的错误,因为编译器不知道用哪个函数模板来推导,所以我们试图将原 sum 与这个 sum 合并,通过模板参数配置来合并。

template <typename T1, typename T2, typename Policy = _sum<T1, T2>>typename Policy::RT sum(T1 value1, T2 value2) {    return Policy()(value1, value2);;}

看上去有点怪,需要在原始代码中指定 _sub 这个 policy,但这并不是我们想要的结果。
观察我们的辅助元函数,它承载了两个功能:转换类型+计算,如果想要使用新的功能,那么作为传递模板参数的 sum 函数,也得要传递新的功能。

template <typename T1, typename T2, typename Policy = _sum<T1, T2>> // 默认用加法typename Policy::RT sum(T1 value1, T2 value2, Policy policy) {    return Policy()(value1, value2);;}int main(int argc, char* argv[]){    short num1 = 1;    int num2 = 2;    auto sumPolicy = _sum<short, int>(); // 加法功能    auto subPolicy = _sub<short, int>(); // 减法功能    cout << sum(num2, num1, sumPolicy) << endl; // 传递功能    cout << sum(num2, num1, subPolicy) << endl; // 传递功能    return 0;}

但是定义功能时显式指定的模板参数并非我们想要的,我们试图在 sum 传递模板参数的时候一并使用上,也就是我们要写出类似这样子的形式:sum(操作对象,操作对象,操作方法),其中的类型,类型转换,都是自动推导出来的

cout << sum(num2, num1, _sum) << endl;cout << sum(num2, num1, _sub) << endl;

是否还记得模板的模板参数这个东西?在函数模板的模板参数自动推导探究中提到,C++11之前函数模板并不支持模板的模板参数,在 C11之后,添加了这个支持,不过我并没有在标准的文档里面找到,也许是我英文太差了(捂脸)
模板的模板参数看起来应该是这个样子的:

template <typename T>class TClass {public:};template <typename T, template <typename TT> class TClass>class TTClass {};int main(int argc, char* argv[]){    TTClass<int, TClass> ttcObject;    return 0;}

如果不用模板的模板参数,其声明的形式应该是 TTClass<int, TClass<int>> ttcObject,现在将它套用到函数模板上,原本的函数改写为:

template <typename T1, typename T2, template <typename TT1, typename TT2> class Policy>typename convert<T1, T2>::value sum(T1 value1, T2 value2, Policy<T1, T2> policy) {    return policy(value1, value2);}int main(int argc, char* argv[]){    cout << sum(1, 2, _sum<int, int>()) << endl;    return 0;}

看出来了吧,跟改之前没有什么区别!(别打我)
我们最大的问题,就是被 policy 的形式给限制住了,我们的 policy 实际上是一个模板类。所以在函数形参和实参匹配的时候一定得显式实例化一个实参。改一下原本的 policy 类:
1、要去掉模板显式声明的过程
2、将类型萃取与计算功能解耦

struct _add {    template <typename T1, typename T2, typename RT = typename convert<T1, T2>::value>    RT operator() (T1 value1, T2 value2) {        return value1+value2;    }};template <typename T1, typename T2, typename Policy>typename convert<T1, T2>::value calculate(T1 value1, T2 value2, Policy policy) {    return policy(value1, value2);}int main(int argc, char* argv[]){    short num1 = 1;    int num2 = 2;    long num3 = 3;    long long num4 = 4;    float num5 = 5.0;    double num6 = 6.0;    long double num7 = 7.0;    cout << calculate(num1, num2, _add()) << endl;    cout << calculate(num1, num1, [](int i, int j){        cout << i << " " << j << endl;        return i+j;    }) << endl;    cout << calculate(num7, num1, [](auto i, auto j){ // warning: generic lambda is C++14 standard!        return i+j;    }) << endl;    return 0;}

这样子不仅可以用仿函数,还可以用函数对象了,这样子添加新的行为的时候只需要写成 policy 的形式即可。

struct add {    template <typename T1, typename T2, typename RT = typename convert<T1, T2>::value>    RT operator() (T1 value1, T2 value2) {        return value1+value2;    }};struct sub {    template <typename T1, typename T2, typename RT = typename convert<T1, T2>::value>    RT operator() (T1 value1, T2 value2) {        return value1-value2;    }};struct mul {    template <typename T1, typename T2, typename RT = typename convert<T1, T2>::value>    RT operator() (T1 value1, T2 value2) {        return value1*value2;    }};struct div {    template <typename T1, typename T2, typename RT = typename convert<T1, T2>::value>    RT operator() (T1 value1, T2 value2) {        return value1/value2; // try catch 就不写了    }};template <typename T1, typename T2, typename Policy>typename convert<T1, T2>::value calculate(T1 value1, T2 value2, Policy policy) {    return policy(value1, value2);}

分析一下

对于一些不同问题的不同处理方式,这些方式遵循不同的逻辑,但是他们却都遵循语法规则,即 n 个值作为输入,某个值作为输出,那么就可以用这样的形式将这些处理方式包装起来,每一个包装的处理方式就是 policy 中的一员。再搭配不同的 trait 的设计,组合出能够应付各种各样的情况的设计机制。

比如:处理一个序列?可能提供这个序列的首尾下标或者指针或者迭代器什么的,可能处理的方式都不相同?

总结一下

trait 注重于类型相关而 policy 注重于行为


CSDN 辣鸡 MD 编辑器,无序列表格式全丢

原创粉丝点击