在C++编程中,特征模板(Trait Templates)是一种设计模式,用来封装类型相关的属性或行为,以便在编译时获取类型信息或者控制模板行为。特征模板有助于简化代码、提高类型安全性以及在模板编程中实现元编程。
实现一个累加序列
为了简化实现这里用指针代替迭代器,首先,写一个初始版本:
#include <iostream>
#include <memory>
#include <vector>
template<typename T>
T accum(T const* beg, T const* end) {
T total{}; // 假设这会产生零值
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
int main() {
std::vector<int> a{1, 2, 3, 4, 5};
int* begin = a.data(), *end = begin + 5; // end 指向末尾元素的后一个
std::cout << accum(begin, end) << std::endl;
return 0;
}
显然可以通过引入一个模板参数 AccT 来解决这个问题,该参数描述了变量 total 使用的类 型 (以及返回类型),就想之前定义的<typename T, typename RT>
一样,但每次使用时,需要额外显示指定返回值类型,比较不方便。
一种解决上述问题的一种方法是,在调用 accum() 的每个类型 T 与应该用于保存累积值的对应类型之间创建关联。这种关联可以认为是 T 类型的特征,因此计算总和的类型有时称为 T 的特征。
当我们特化AccumulationTraits时,可以指定累积类型为int,这是因为char类型的累加结果可能超出char本身的范围,需要更大的类型来容纳。这样,每当accum()函数模板在处理类型T的序列时,就能够通过typename AccumulationTraits::AccT自动获取正确的累积值类型,确保累加过程不会出现意料之外的问题。
#include <iostream>
#include <memory>
#include <vector>
// 定义一系列特征模板
template<typename T>
struct AccumulationTraits;
template<>
struct AccumulationTraits<char> { using AccT = int; };
template<>
struct AccumulationTraits<short> {using AccT = int; };
template<>
struct AccumulationTraits<int> { using AccT = long; };
template<>
struct AccumulationTraits<unsigned int> { using AccT = unsigned long; };
template<>
struct AccumulationTraits<float> { using AccT = double; };
// 使用特征模板获取指定类型,并使用auto推导返回类型
template<typename T>
auto accum(T const* beg, T const* end) {
using AccT = typename AccumulationTraits<T>::AccT; // 获取指定的类型
AccT total{}; // 假设这会产生零值
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
int main() {
std::vector<int> a{1, 2, 3, 4, 5};
int* begin = a.data(), *end = begin + 5; // end 指向末尾元素的后一个
std::cout << accum(begin, end) << std::endl; // 符合预期,输出15
char b[] = "templates";
int length = sizeof(b)-1;
char* begin2 = b, *end2 = b+length;
// 模板为 char 类型实例化,结果是对于相对较小的值的积累来说,其范围 太小。
std::cout << accum(begin2, end2) << std::endl;
return 0;
}
但是这需要保证 AccT total{}; 会产生一个零值,对于基本类型,可能这是正确的,但是对于自定义类型,就未必了,所以,特征模板的写法可以进行改进:
#include <iostream>
#include <memory>
#include <vector>
// 定义一系列特征模板
template<typename T>
struct AccumulationTraits;
template<>
struct AccumulationTraits<char> {
using AccT = int;
static constexpr AccT zero() { return 0; }
};
template<>
struct AccumulationTraits<int> {
using AccT = long;
static constexpr AccT zero() { return 0; }
};
// 使用特征模板获取指定类型,并使用auto推导返回类型
template<typename T>
auto accum(T const* beg, T const* end) {
using AccT = typename AccumulationTraits<T>::AccT; // 获取指定的类型
AccT total = AccumulationTraits<T>::zero(); // 保证特征变量的初始化
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
int main() {
std::vector<int> a{1, 2, 3, 4, 5};
int* begin = a.data(), *end = begin + 5; // end 指向末尾元素的后一个
std::cout << accum(begin, end) << std::endl; // 符合预期,输出15
char b[] = "templates";
int length = sizeof(b)-1;
char* begin2 = b, *end2 = b+length;
// 模板为 char 类型实例化,结果是对于相对较小的值的积累来说,其范围 太小。
std::cout << accum(begin2, end2) << std::endl;
return 0;
}
和之前的区别是使用了函数调用语法 (而不是对静态数据成员更简单的访问)。
上面的代码还有一些改进空间,对于绝大多数用户,使用默认的返回值即可,但对于一些用户,可能希望能够自定义返回值,那么,函数接口可以设计为:
#include <iostream>
#include <memory>
#include <vector>
// 定义一系列特征模板
template<typename T>
struct AccumulationTraits;
template<>
struct AccumulationTraits<int> {
using AccT = long;
static constexpr AccT zero() { return 0; }
};
template<>
struct AccumulationTraits<float> {
using AccT = double;
static constexpr AccT zero() { return 0; }
};
// 对于大多数用户,可通过传递特征模板参数来覆盖默认的Traits
template<typename T, typename AT = AccumulationTraits<T>>
auto accum(T const* beg, T const* end) {
using AccT = typename AT::AccT; // 获取指定的类型
AccT total = AccumulationTraits<T>::zero(); // 保证特征变量的初始化
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
int main() {
std::vector<float> a{1.2, 2.3, 3.5, 4.1, 5};
float* begin = a.data(), *end = begin + 5; // end 指向末尾元素的后一个
std::cout << accum(begin, end) << std::endl; // 符合预期,输出16.1
// 使用自定义的Traits, 得到long类型的返回值
std::cout << accum<float,AccumulationTraits<int>>(begin, end) << std::endl;
return 0;
}
特征、策略和策略类
特征(Traits): 特征模板是一种设计模式,用于捕获和表达模板参数类型的相关属性,如类型的安全转换规则、默认初始值、大小等。例如,在累加序列的背景下,AccumulationTraits就是一个特征模板,它定义了累加过程中的累积类型AccT以及如何初始化累积器到零值的函数zero()。特征通常通过模板特化为不同类型的用户提供定制化的信息,便于编译时做出决策。
策略(Policies): 用于指导模板中的算法执行某种特定的行为或操作。在累加的例子中,策略类定义了一系列数值的具体逻辑。策略类通常包含静态成员函数,如accumulate(),它们接受当前累积值和要加入的新值,并根据策略实施相应的累积操作(如加法或乘法)。策略可以通过模板参数传递到泛型函数中,从而允许用户在不改变函数主体的前提下更换累积逻辑。在累加操作中, total += *beg 就是一种策略, 但是显然它还不够通用,并不是所有对象都会有*操作,所以,上述可以优化为:
#include <iostream>
#include <memory>
#include <vector>
// 定义一系列特征模板
template<typename T>
struct AccumulationTraits;
template<>
struct AccumulationTraits<int> {
using AccT = long;
static constexpr AccT zero() { return 0; }
};
template<>
struct AccumulationTraits<float> {
using AccT = double;
static constexpr AccT zero() { return 0; }
};
// 策略模板, 累加
struct SumPolicy{
template<typename T1, typename T2>
static void accumulate(T1& total, T2 const& value) {
total += value;
}
};
// 策略模板 累乘
struct MultPolicy{
template<typename T1, typename T2>
static void accumulate(T1& total, T2 const& value) {
total *= value;
}
};
template<typename T,
typename Policy = SumPolicy,
typename Traits = AccumulationTraits<T>>
auto accum(T const* beg, T const* end) {
using AccT = typename Traits::AccT; // 获取指定的类型
AccT total = Traits::zero(); // 保证特征变量的初始化
while (beg != end) {
Policy::accumulate(total, *beg);;
++beg;
}
return total;
}
int main() {
std::vector<float> a{1.1, 2, 3, 4, 5};
float* begin = a.data(), *end = begin + 5; // end 指向末尾元素的后一个
std::cout << accum(begin, end) << std::endl; // 符合预期,输出15.1
// 使用自定义的特征模板 : 15
std::cout << accum<float, SumPolicy,AccumulationTraits<int>>(begin, end) << std::endl;
// 使用自定义策略模板:0 , 因为total 初始化是0
std::cout << accum<float, MultPolicy>(begin, end) << std::endl;
return 0;
}
这里的问题是由初值的选择引起的: 尽管 0 适用于求和,但它不适用于乘法 (零初值会导致累积 乘法的结果为零)。这说明了不同的特征和政策可能会相互影响。
一种解决办法是在accum接口开放初始值,默认为:Traits::zero(),也可以自定义:
#include <iostream>
#include <memory>
#include <vector>
// 定义一系列特征模板
template<typename T>
struct AccumulationTraits;
template<>
struct AccumulationTraits<int> {
using AccT = long;
static constexpr AccT zero() { return 0; }
};
template<>
struct AccumulationTraits<float> {
using AccT = double;
static constexpr AccT zero() { return 0; }
};
// 策略模板, 累加
struct SumPolicy{
template<typename T1, typename T2>
static void accumulate(T1& total, T2 const& value) {
total += value;
}
};
// 策略模板 累乘
struct MultPolicy{
template<typename T1, typename T2>
static void accumulate(T1& total, T2 const& value) {
total *= value;
}
};
template<typename T,
typename Policy = SumPolicy,
typename Traits = AccumulationTraits<T>,
typename AccT = typename Traits::AccT >
auto accum(T const* beg, T const* end, AccT total = Traits::zero()) {
while (beg != end) {
Policy::accumulate(total, *beg);;
++beg;
}
return total;
}
int main() {
std::vector<float> a{1.1, 2, 3, 4.2, 5};
float* begin = a.data(), *end = begin + 5; // end 指向末尾元素的后一个
// std::cout << accum(begin, end) << std::endl; // 符合预期,输出15.1
// 使用自定义策略模板, 初始默认为1.0 : 结果为120
std::cout << accum<float, MultPolicy, AccumulationTraits<float>>(begin, end, 1.0) << std::endl;
return 0;
}
总结:
特征类: 用来代替模板参数的类。作为一个类,聚合有用的类型和常量; 作为模板,为解决所有 “间接级”软件问题的提供了一条途径。
• 特征表示模板参数的自然属性。
• 策略表示泛型函数和类型的可配置行为 (通常带有一些常用的默认值)。 为了进一步阐述这两个概念之间的区别,我们列出了以下关于特征的观察结果:
• 特征可以固化使用 (例如,不需要通过模板参数传递)。
• 特征参数通常有非常自然的默认值 (很少重写,或者不能重写)。
• 特征参数往往紧密地依赖于一个或多个主要参数。
• 特征主要组合类型和常量,而不是成员函数。
• 特征倾向于在特征模板中进行收集。
对于策略类,有以下观察:
• 若策略类没有作为模板参数传递,那其作用不大。
• 策略参数不需要有默认值,通常显式指定 (尽管许多通用组件都配置了常用的默认策略)。
• 策略参数大多与模板的其他参数无关。
• 策略类通常组合成员函数。
• 策略可以在普通类或类模板中进行收集。