模板元编程实例—如何设计通用的几何库
设计原理
假设你需要使用c++程序来计算两点间的距离.你可能会这样做:
先定义一个
struct
:1
2
3
4struct mypoint
{
double x, y;
};然后定义一个包含计算算法的函数:
1
2
3
4
5
6double distance(mypoint const& a, mypoint const& b)
{
double dx = a.x - b.x;
double dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
相当简单而实用,但是不够通用.一个库的设计需要考虑未来可能的变化.
上面的设计只能用于笛卡尔坐标系中的2D点.
通用的库需要能够计算如下距离:
- 适用于任何
point struct
或者point class
,而不是只适用于mypoint
. - 不只是二维
- 适用于其它坐标系统,如地球或球体上
- 能够计算
点与线
或者其它几何图形
之间的距离 - 比
double
更高的精度 - 尽可能避免使用
sqrt
:通常我们不希望调用它,因为它的开销比较大.而且对于比较距离时没有必要.
接下来,我们将一步一步给出一个更通用的实现.
使用模板
我们可以将距离函数改为模板函数.这样就可以计算除mypoint
之外的其他点类型之间的距离.
我们添加两个模板参数,允许输入两种不同的点类型.
1 | template <typename P1, typename P2> |
模板版本
比之前的实现好一些,但是还不够.
考虑c++类
的成员变量为protected
或者不能直接访问x
,y
.
使用Traits
我们需要使用一种更通用的方法来允许任意的点类型都能够作为距离函数的输入.
除了直接访问x
和y
,我们将添加一层间接层,使用traits
系统.
距离函数可以变为:
1 | template <typename P1, typename P2> |
上面的距离函数使用了get
函数来访问一个点的坐标系统,使用点的维度
作为模板参数.get
可以这样实现:
1 | namespace traits |
定义mypoint
的模板特例:
1 | namespace traits |
现在通过调用traits::access<mypoint, 0>::get(a)
就可以返回坐标系中的x
.我们可以通过定义get
来进一步简化调用方式:
1 | template <int D, typename P> |
通过上面的实现,我们就可以对任何特化了traits::access
的point a
调用get<0>(a)
.
同样的原理,我们也可以实现对于坐标y
的get<1>(a)
.
任意维度
为了实现对任意维度的计算,我们可以通过循环来遍历所有维度.但是循环调用相对于直接计算会有性能开销.因此我们可以通过使用模板实现这样的算法:
1 | template <typename P1, typename P2, int D> |
然后距离函数
可以调用pythagoras
并指定维度:
1 | template <typename P1, typename P2> |
维度可以通过定义另外一个traits
类来实现:
1 | namespace traits |
然后针对相应的类(如mypoint
)进行特例化,因为这个traits
只是发布一个值,因此为了简便我们可以继承Boost.MPL
中的class boost::mpl::int_
:
1 | namespace traits |
现在我们就实现了对任意维度点进行计算距离的算法.我们还使用编译期断言来防止对两个不同维度的点进行计算.
坐标类型
在上面的实现中,我们假设了double
类型,如果点是integer
呢?
1 | namespace traits |
和access
函数类似,我们同样添加一个代理:
1 | template <typename P> |
然后我们可以修改我们的距离计算函数.因为计算的两个point类型可能有不同的类型,我们必须处理这种情况.我们需要选择其中一种具有更高精度的类型作为结果类型,我们假设有一个select_most_precise
元函数用于选择最佳类型.
这样我们的计算函数可以改为:
1 | template <typename P1, typename P2, int D> |
不同的形状
我们已经设计了一个支持任意维度和任意坐标系统中的点的实现.
现在我们需要看看如何支持计算点与多边形或者点与线之间的距离.
支持这些形式对之前的设计会有较大的影响,我们不想添加另外一个名称的函数,如:
1 | template <typename P, typename S> |
我们想更加通用,距离函数的调用者最好不用关心形状的类型,我们也无法通过重载类实现,因为模板的签名相同,会有二义性.
有两种解决方法:
- tag dispatching
- SFINAE
在这里,我们选择tag dispatching
因为它适合于`traits`系统.
使用tag dispatching
,距离计算算法检查输入的几何形状类型.
我们的距离函数将变成:
1 | template <typename G1, typename G2> |
使用tag
元函数获取类型然后将调用转交给dispatch::distance
的apply
方法.tag
元函数是另一个traits
类,需要被point
类特例化:
1 | namespace traits |
Tags (point_tag, segment_tag, etc)是用于特例化dispatch struct
的空结构.distance
的dispatch struct
和其特例化都定义于另外一个单独的命名空间中:
1 | namespace dispatch { |
现在,距离算法对所有不同的几何形状都是通用的.
还有一个缺点是:我们必须为point,segment特例化2个dispatch.
1 | point a(1,1); |