第 19 章 Traits的实现(一)

在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;
}

总结:

特征类: 用来代替模板参数的类。作为一个类,聚合有用的类型和常量; 作为模板,为解决所有 “间接级”软件问题的提供了一条途径。

• 特征表示模板参数的自然属性。
• 策略表示泛型函数和类型的可配置行为 (通常带有一些常用的默认值)。 为了进一步阐述这两个概念之间的区别,我们列出了以下关于特征的观察结果:
• 特征可以固化使用 (例如,不需要通过模板参数传递)。
• 特征参数通常有非常自然的默认值 (很少重写,或者不能重写)。
• 特征参数往往紧密地依赖于一个或多个主要参数。
• 特征主要组合类型和常量,而不是成员函数。
• 特征倾向于在特征模板中进行收集。

对于策略类,有以下观察:
• 若策略类没有作为模板参数传递,那其作用不大。
• 策略参数不需要有默认值,通常显式指定 (尽管许多通用组件都配置了常用的默认策略)。
• 策略参数大多与模板的其他参数无关。
• 策略类通常组合成员函数。
• 策略可以在普通类或类模板中进行收集。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值