19.1 Traits编程模式
相关章节
在本节中你将学到…
- 什么是Traits模式,以及它与Java/C#接口的本质区别
- CGAL中Traits如何解耦算法与几何内核
- 如何编写自定义Traits类
- 类型推导和类型萃取的实际应用
- 通过Traits实现代码复用的设计技巧
19.1.0 类比解释:餐厅的菜单系统
生活场景类比
想象你去一家国际化餐厅吃饭。这家餐厅提供中、法、意、日四种菜系。服务员(算法)需要了解每道菜的烹饪方式,但他不需要亲自掌握所有烹饪技巧。
传统方式(无Traits):
- 服务员必须记住每种菜的具体做法
- 新增菜系时,所有服务员都要重新培训
- 换一批服务员,所有培训重来
Traits方式:
- 每道菜附带”烹饪说明书”(Traits)
- 服务员只需阅读说明书即可服务
- 新增菜系?只需提供新的说明书
- 换服务员?新员工会读说明书即可
关键洞察:Traits将”能力说明”从”执行者”分离,实现算法与数据结构的解耦。
为什么需要Traits模式?
问题场景1:算法与几何内核的紧耦合
// 传统方式:算法直接依赖具体类型
void convex_hull(std::vector<Point_2<Cartesian<double>>>& points) {
// 只能处理double精度的笛卡尔坐标点
// 想改用精确算术?重写整个算法!
}问题场景2:不同算法需要不同信息
// 凸包算法需要:
// - 点的比较(左右转测试)
// - 方向判断
// Delaunay三角剖分需要:
// - 上述所有
// - 外接圆测试
// - 距离计算
// 如何优雅地表达这些需求?问题场景3:编译期优化
// Traits使得所有类型信息在编译期确定:
// - 无虚函数调用开销
// - 编译器可以内联所有操作
// - 零运行时开销的抽象19.1.1 概念解释
什么是Traits模式?
Traits(特征)模式是C++模板编程中的一种核心技术,它允许我们将类型的相关信息和行为从类型本身分离出来。可以把Traits想象成类型的”身份证”或”能力说明书”——它描述了某个类型具备哪些特性、支持哪些操作。
类比理解:想象你是一位厨师(算法),需要处理各种食材(几何对象)。Traits就像是一本食材百科全书,告诉你每种食材的切法、烹饪时间和最佳搭配。你不需要知道食材的具体品种,只需要查阅这本百科全书即可。
Traits vs 接口(Java/C#)
这是C++程序员最容易混淆的概念:
| 特性 | Java/C# 接口 | C++ Traits |
|---|---|---|
| 实现方式 | 运行时多态(虚函数表) | 编译期多态(模板) |
| 性能开销 | 有(虚函数调用) | 无(内联展开) |
| 类型检查 | 运行时 | 编译期 |
| 扩展性 | 需要继承 | 无需修改原类型 |
| 信息类型 | 主要是行为 | 类型信息 + 行为 |
关键区别:接口说”我能做什么”,Traits说”我是什么,我能做什么,以及如何做”。
图示对比:
Java/C# 接口(运行时多态):
算法 ──> 接口(虚函数表)
│
┌───────┼───────┐
↓ ↓ ↓
实现A 实现B 实现C
运行时开销:虚函数调用
编译期:无法内联
C++ Traits(编译期多态):
算法 ──> Traits模板
│
┌───────┼───────┐
↓ ↓ ↓
类型A 类型B 类型C
编译期:完全展开和内联
运行时:零开销
图示:Traits的工作方式
┌─────────────────────────────────────────────────────────────┐
│ 算法 (Algorithm) │
│ template <typename Traits> │
│ void compute(Traits::Point p, Traits::Point q) { │
│ Traits::Compute_squared_distance_2 dist; │
│ auto d = dist(p, q); │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Traits 层 │
│ template <typename Kernel> │
│ struct MyTraits { │
│ typedef Kernel::Point_2 Point; │
│ typedef Kernel::Compute_squared_distance_2 │
│ Compute_squared_distance_2; │
│ }; │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 具体内核 │
│ - Cartesian<double> │
│ - Cartesian<Gmpq> │
│ - Homogeneous<leda_integer> │
└─────────────────────────────────────────────────────────────┘
19.1.2 为什么需要Traits?
问题1:算法与数据结构的紧耦合
假设我们要实现一个凸包算法:
// 错误的设计:算法直接依赖具体类型
void convex_hull(std::vector<Point_2<Cartesian<double>>>& points) {
// 只能处理 double 精度的笛卡尔坐标点
}问题:如果我们想用精确算术(Gmpq)或齐次坐标,必须重写整个算法!
问题2:不同算法需要不同的几何信息
凸包算法需要:
- 点的比较(左右转测试)
- 方向判断
Delaunay三角剖分需要:
- 上述所有
- 外接圆测试
- 距离计算
解决方案:Traits允许算法声明”我需要这些能力”,由调用者提供具体的实现。
问题3:编译期优化
通过Traits,所有类型信息在编译期确定:
- 无虚函数调用开销
- 编译器可以内联所有操作
- 零运行时开销的抽象
19.1.3 代码示例
基础示例:最简单的Traits
#include <iostream>
#include <type_traits>
// ============================================
// 基础Traits:描述类型的基本属性
// ============================================
// 通用Traits模板(主模板)
template <typename T>
struct MyTraits {
// 默认情况下,类型不是浮点类型
static constexpr bool is_floating_point = false;
// 默认比较操作
static bool equal(const T& a, const T& b) {
return a == b;
}
};
// double类型的特化
template <>
struct MyTraits<double> {
static constexpr bool is_floating_point = true;
static constexpr double epsilon = 1e-10;
// 浮点数需要特殊比较(考虑精度误差)
static bool equal(double a, double b) {
return std::abs(a - b) < epsilon;
}
};
// int类型的特化
template <>
struct MyTraits<int> {
static constexpr bool is_floating_point = false;
static bool equal(int a, int b) {
return a == b; // 整数可以直接比较
}
};
// 使用Traits的通用函数
template <typename T>
bool safe_equal(const T& a, const T& b) {
// 通过Traits获取类型的比较方式
return MyTraits<T>::equal(a, b);
}
int main() {
// 测试浮点数比较
double x = 0.1 + 0.2; // 实际上不等于 0.3
double y = 0.3;
std::cout << "Direct comparison: " << (x == y) << std::endl; // 0 (false)
std::cout << "Traits comparison: " << safe_equal(x, y) << std::endl; // 1 (true)
// 测试整数比较
int a = 5, b = 5;
std::cout << "Int comparison: " << safe_equal(a, b) << std::endl; // 1 (true)
return 0;
}可视化:
编译期选择:
safe_equal(0.1+0.2, 0.3)
↓
MyTraits<double>::equal(0.300..., 0.300...)
↓
|0.300... - 0.300...| < 1e-10 ?
↓
true(考虑浮点误差)
vs
safe_equal(5, 5)
↓
MyTraits<int>::equal(5, 5)
↓
5 == 5
↓
true(直接比较)
中级示例:几何Traits
#include <iostream>
#include <cmath>
// ============================================
// 几何Traits:解耦算法与几何实现
// ============================================
// 简单的2D点(笛卡尔坐标)
struct CartesianPoint2D {
double x, y;
CartesianPoint2D(double x = 0, double y = 0) : x(x), y(y) {}
};
// 简单的2D点(极坐标)
struct PolarPoint2D {
double r, theta; // 半径和角度
PolarPoint2D(double r = 0, double theta = 0) : r(r), theta(theta) {}
};
// ============================================
// Cartesian点的Traits
// ============================================
template <>
struct GeometryTraits<CartesianPoint2D> {
typedef CartesianPoint2D Point;
typedef double Coordinate_type;
// 计算两点间距离的平方
static double squared_distance(const Point& p1, const Point& p2) {
double dx = p1.x - p2.x;
double dy = p1.y - p2.y;
return dx * dx + dy * dy;
}
// 判断三点方向(左转、右转、共线)
// 返回值 > 0: 左转,< 0: 右转,= 0: 共线
static double orientation(const Point& p, const Point& q, const Point& r) {
return (q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x);
}
// 获取x坐标
static double x(const Point& p) { return p.x; }
static double y(const Point& p) { return p.y; }
};
// ============================================
// Polar点的Traits
// ============================================
template <>
struct GeometryTraits<PolarPoint2D> {
typedef PolarPoint2D Point;
typedef double Coordinate_type;
// 极坐标需要转换后计算距离
static double squared_distance(const Point& p1, const Point& p2) {
// 转换为笛卡尔坐标再计算
double x1 = p1.r * std::cos(p1.theta);
double y1 = p1.r * std::sin(p1.theta);
double x2 = p2.r * std::cos(p2.theta);
double y2 = p2.r * std::sin(p2.theta);
double dx = x1 - x2;
double dy = y1 - y2;
return dx * dx + dy * dy;
}
// 极坐标的方向判断(同样需要先转换)
static double orientation(const Point& p, const Point& q, const Point& r) {
auto to_cartesian = [](const Point& pt) {
return std::make_pair(pt.r * std::cos(pt.theta),
pt.r * std::sin(pt.theta));
};
auto [px, py] = to_cartesian(p);
auto [qx, qy] = to_cartesian(q);
auto [rx, ry] = to_cartesian(r);
return (qx - px) * (ry - py) - (qy - py) * (rx - px);
}
static double x(const Point& p) { return p.r * std::cos(p.theta); }
static double y(const Point& p) { return p.r * std::sin(p.theta); }
};
// 前置声明通用Traits模板
template <typename T>
struct GeometryTraits;
// ============================================
// 使用Traits的通用算法
// ============================================
template <typename Point>
bool is_left_turn(const Point& p, const Point& q, const Point& r) {
// 算法不关心Point是笛卡尔还是极坐标
// 所有操作都通过Traits完成
return GeometryTraits<Point>::orientation(p, q, r) > 0;
}
template <typename Point>
double distance(const Point& p1, const Point& p2) {
return std::sqrt(GeometryTraits<Point>::squared_distance(p1, p2));
}
int main() {
// 使用笛卡尔坐标
CartesianPoint2D c1(0, 0), c2(1, 0), c3(0.5, 1);
std::cout << "Cartesian left turn: "
<< is_left_turn(c1, c2, c3) << std::endl; // 1 (true)
// 使用极坐标(同样的算法!)
PolarPoint2D p1(0, 0), p2(1, 0), p3(std::sqrt(1.25), std::atan2(1, 0.5));
std::cout << "Polar left turn: "
<< is_left_turn(p1, p2, p3) << std::endl; // 1 (true)
return 0;
}可视化:
算法:is_left_turn(p, q, r)
情况1:CartesianPoint2D
↓
GeometryTraits<CartesianPoint2D>::orientation
↓
直接计算:(q.x-p.x)*(r.y-p.y) - (q.y-p.y)*(r.x-p.x)
情况2:PolarPoint2D
↓
GeometryTraits<PolarPoint2D>::orientation
↓
转换到笛卡尔坐标:
p → (p.r*cos(p.θ), p.r*sin(p.θ))
q → (q.r*cos(q.θ), q.r*sin(q.θ))
r → (r.r*cos(r.θ), r.r*sin(r.θ))
↓
计算叉积
关键:同一套算法,通过Traits适配不同坐标系!
高级示例:完整的CGAL风格Traits
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
// ============================================
// 完整的CGAL风格Traits实现
// ============================================
// 内核概念:提供几何对象和基本操作
template <typename Kernel_>
struct ConvexHullTraits {
typedef Kernel_ Kernel;
// 从内核导入类型
typedef typename Kernel::Point_2 Point_2;
typedef typename Kernel::Less_xy_2 Less_xy_2;
typedef typename Kernel::Left_turn_2 Left_turn_2;
typedef typename Kernel::Equal_2 Equal_2;
// 创建函数对象实例
Less_xy_2 less_xy_2_object() const { return Less_xy_2(); }
Left_turn_2 left_turn_2_object() const { return Left_turn_2(); }
Equal_2 equal_2_object() const { return Equal_2(); }
};
// ============================================
// 简单的笛卡尔内核实现
// ============================================
struct SimpleCartesianKernel {
struct Point_2 {
double x, y;
Point_2(double x = 0, double y = 0) : x(x), y(y) {}
};
// 函数对象:按字典序比较点
struct Less_xy_2 {
bool operator()(const Point_2& p, const Point_2& q) const {
return p.x < q.x || (p.x == q.x && p.y < q.y);
}
};
// 函数对象:判断左转
struct Left_turn_2 {
bool operator()(const Point_2& p,
const Point_2& q,
const Point_2& r) const {
double area = (q.x - p.x) * (r.y - p.y) -
(q.y - p.y) * (r.x - p.x);
return area > 0;
}
};
// 函数对象:判断相等
struct Equal_2 {
bool operator()(const Point_2& p, const Point_2& q) const {
return p.x == q.x && p.y == q.y;
}
};
};
// ============================================
// 使用Traits的凸包算法(Graham扫描)
// ============================================
template <typename Traits>
std::vector<typename Traits::Point_2>
convex_hull_2(std::vector<typename Traits::Point_2>& points,
const Traits& traits = Traits()) {
typedef typename Traits::Point_2 Point_2;
if (points.size() <= 1) return points;
// 获取Traits提供的函数对象
typename Traits::Less_xy_2 less_xy = traits.less_xy_2_object();
typename Traits::Left_turn_2 left_turn = traits.left_turn_2_object();
typename Traits::Equal_2 equal = traits.equal_2_object();
// 找到最左下角的点作为起点
auto min_it = std::min_element(points.begin(), points.end(), less_xy);
std::iter_swap(points.begin(), min_it);
Point_2 pivot = points[0];
// 按极角排序其余点
std::sort(points.begin() + 1, points.end(),
[&pivot, &left_turn](const Point_2& a, const Point_2& b) {
// 如果共线,按距离排序
if (\!left_turn(pivot, a, b) && \!left_turn(pivot, b, a)) {
double da = (a.x - pivot.x) * (a.x - pivot.x) +
(a.y - pivot.y) * (a.y - pivot.y);
double db = (b.x - pivot.x) * (b.x - pivot.x) +
(b.y - pivot.y) * (b.y - pivot.y);
return da < db;
}
return left_turn(pivot, a, b);
});
// Graham扫描
std::vector<Point_2> hull;
for (const auto& p : points) {
while (hull.size() >= 2 &&
\!left_turn(hull[hull.size()-2], hull.back(), p)) {
hull.pop_back();
}
hull.push_back(p);
}
return hull;
}
int main() {
// 定义Traits类型
typedef ConvexHullTraits<SimpleCartesianKernel> CHTraits;
// 创建测试数据
std::vector<SimpleCartesianKernel::Point_2> points = {
{0, 0}, {1, 0}, {0.5, 0.5}, {1, 1}, {0, 1}, {0.5, 0.2}
};
// 计算凸包
auto hull = convex_hull_2(points, CHTraits());
std::cout << "Convex hull vertices: " << hull.size() << std::endl;
for (const auto& p : hull) {
std::cout << "(" << p.x << ", " << p.y << ")" << std::endl;
}
return 0;
}19.1.4 CGAL中的应用
Kernel_traits:获取类型的内核
CGAL使用Traits模式来关联几何对象与其内核:
// 来自 CGAL/Kernel_traits.h
namespace CGAL {
template <class T>
struct Kernel_traits {
// 默认情况下,假设类型有嵌套的R类型
typedef typename T::R Kernel;
typedef Kernel type;
};
} // namespace CGAL使用场景:当你有一个Point_2对象,需要知道它属于哪个内核时:
Point_2<Exact_predicates_inexact_constructions_kernel> p;
typedef Kernel_traits<Point_2>::Kernel K; // 获取内核类型算法Traits:凸包示例
CGAL的凸包算法使用ConvexHullTraits_2概念:
// 简化的CGAL凸包Traits概念
template <typename Traits>
void convex_hull_2(Iterator first, Iterator last,
OutputIterator result,
const Traits& traits); // 通过Traits参数传入CGAL提供的默认Traits就是内核本身,因为内核已经实现了所有需要的操作。
自定义Traits:适配外部数据结构
// 假设你有一个遗留系统的点类型
struct LegacyPoint {
float coord[2];
int id;
};
// 创建Traits来适配CGAL算法
struct LegacyPointTraits {
typedef LegacyPoint Point_2;
struct Less_xy_2 {
bool operator()(const LegacyPoint& p, const LegacyPoint& q) const {
return p.coord[0] < q.coord[0] ||
(p.coord[0] == q.coord[0] && p.coord[1] < q.coord[1]);
}
};
struct Left_turn_2 {
bool operator()(const LegacyPoint& p,
const LegacyPoint& q,
const LegacyPoint& r) const {
double area = (q.coord[0] - p.coord[0]) * (r.coord[1] - p.coord[1]) -
(q.coord[1] - p.coord[1]) * (r.coord[0] - p.coord[0]);
return area > 0;
}
};
Less_xy_2 less_xy_2_object() const { return Less_xy_2(); }
Left_turn_2 left_turn_2_object() const { return Left_turn_2(); }
};
// 现在可以直接对LegacyPoint使用CGAL凸包算法!
std::vector<LegacyPoint> legacy_points = ...;
std::vector<LegacyPoint> hull;
CGAL::convex_hull_2(legacy_points.begin(), legacy_points.end(),
std::back_inserter(hull),
LegacyPointTraits());19.1.5 常见陷阱
陷阱1:忘记特化
// 错误:使用主模板处理需要特殊行为的类型
template <typename T>
struct MyTraits {
static T zero() { return T(0); } // 对指针类型会出错!
};
// 正确:为指针类型提供特化
template <typename T>
struct MyTraits<T*> {
static T* zero() { return nullptr; }
};陷阱2:Traits与类型循环依赖
// 错误:循环依赖
struct MyPoint {
typedef MyTraits<MyPoint>::Coordinate_type Coordinate_type; // 错误!
Coordinate_type x, y;
};
template <>
struct MyTraits<MyPoint> {
typedef double Coordinate_type;
};
// 正确:先声明Traits,再定义类型
template <>
struct MyTraits<MyPoint> {
typedef double Coordinate_type;
};
struct MyPoint {
typedef MyTraits<MyPoint>::Coordinate_type Coordinate_type;
Coordinate_type x, y;
};陷阱3:在Traits中使用非静态成员
// 错误:Traits应该是无状态的
template <typename T>
struct BadTraits {
int precision; // 错误!Traits不应该有状态
bool equal(T a, T b) {
return std::abs(a - b) < precision;
}
};
// 正确:状态应该通过函数对象传递
template <typename T>
struct GoodTraits {
struct Equal {
int precision;
Equal(int p) : precision(p) {}
bool operator()(T a, T b) const {
return std::abs(a - b) < precision;
}
};
static Equal create_equal(int precision) {
return Equal(precision);
}
};陷阱4:过度使用Traits
// 过度设计:简单的类型信息不需要Traits
template <typename T>
struct OverkillTraits {
typedef T value_type;
static size_t size() { return sizeof(T); }
};
// 简单场景直接用type_traits
#include <type_traits>
typedef typename std::remove_reference<T>::type ValueType;19.1.6 最佳实践
实践1:遵循STL风格
// 像STL一样组织Traits
template <typename Iterator>
struct iterator_traits {
typedef typename Iterator::value_type value_type;
typedef typename Iterator::difference_type difference_type;
typedef typename Iterator::pointer pointer;
typedef typename Iterator::reference reference;
typedef typename Iterator::iterator_category iterator_category;
};
// 为指针提供特化
template <typename T>
struct iterator_traits<T*> {
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef T& reference;
typedef std::random_access_iterator_tag iterator_category;
};实践2:使用默认模板参数
// 允许用户自定义,同时提供合理的默认值
template <typename Point,
typename Traits = DefaultGeometryTraits<Point>>
class PointSet {
// 使用Traits...
};
// 用户可以使用默认Traits
PointSet<CartesianPoint> set1;
// 或提供自定义Traits
PointSet<LegacyPoint, LegacyPointTraits> set2;实践3:Traits的继承和组合
// 基础几何Traits
template <typename Kernel>
struct BasicGeometryTraits {
typedef typename Kernel::Point_2 Point_2;
typedef typename Kernel::Segment_2 Segment_2;
};
// 扩展Traits(继承基础)
template <typename Kernel>
struct AdvancedGeometryTraits : BasicGeometryTraits<Kernel> {
typedef typename Kernel::Circle_2 Circle_2;
typedef typename Kernel::Triangle_2 Triangle_2;
};
// 组合多个Traits
template <typename GeometryTraits, typename NumericTraits>
struct CombinedTraits : GeometryTraits, NumericTraits {
// 组合功能
};实践4:文档化Traits需求
/**
* @brief 计算凸包
*
* @tparam Traits 必须满足以下概念:
* - 定义 Point_2 类型
* - 提供 Less_xy_2 函数对象,用于比较点
* - 提供 Left_turn_2 函数对象,用于方向判断
* - 提供 less_xy_2_object() 等创建函数
*/
template <typename Traits>
void convex_hull_2(...);本节要点
-
Traits是编译期接口:与Java/C#的运行时接口不同,Traits在编译期解析,零运行时开销。
-
解耦的关键:Traits解耦了算法与具体数据类型,使得同一算法可以处理多种数据表示。
-
模板特化的威力:通过模板全特化和偏特化,可以为不同类型提供定制化的行为。
-
CGAL的核心设计:Kernel_traits、算法Traits等都是Traits模式的典型应用,理解它们有助于深入理解CGAL架构。
-
避免过度设计:Traits适用于需要类型信息或行为的抽象,简单的类型操作可以直接使用标准type_traits。
-
函数对象优于函数指针:在Traits中使用函数对象(仿函数)可以获得更好的内联优化。
进一步阅读
- 《C++ Template Metaprogramming》 by David Abrahams and Aleksey Gurtovoy
- CGAL文档:Kernel Manual中的”Kernel Concept”章节
- Boost.TypeTraits:标准type_traits库的设计参考