欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

STL萃取(Traits)机制

程序员文章站 2022-03-23 10:47:13
...

问题前述

我们希望有一个类,并有一个成员方法 GetSum 可以完成数组元素的一系列运算。具体看如下代码:

#include <iostream>
using namespace std;

//处理int类型
class IntArray
{
public:
    IntArray()
    {
        a = new int[10];
        for (int i = 0; i < 10; ++i)
        {
            a[i] = i + 1;
        }
    }
    ~IntArray()
    {
        delete[] a;
    }

    int GetSum(int times)
    {
        int sum = 0;
        for (int i = 0; i < 10; ++i)
            sum += a[i];
        cout << "int sum=" << sum << endl;
        return sum * times;
    }

private:
    int *a;
};

//处理float类型
class FloatArray
{
public:
    FloatArray()
    {
        f = new float[10];
        for (int i = 1; i <= 10; ++i)
        {
            f[i - 1] = 1.0f / i;
        }
    }
    ~FloatArray()
    {
        delete[] f;
    }
    float GetSum(float times)
    {
        float sum = 0.0f;
        for (int i = 0; i < 10; i++)
            sum += f[i];
        cout << "float sum=" << sum << endl;
        return sum * times;
    }

private:
    float *f;
};

//通过模板类型推导,调用不同类型的GetSum函数
template <class T>
class Apply
{
public:
    float GetSum(T &t, float inarg)
    {
        return t.GetSum(inarg);
    }
};

int main()
{
    Apply<IntArray> iapply;
    IntArray iarr;
    cout << iapply.GetSum(iarr, 2) << endl;

    Apply<FloatArray> fapply;
    FloatArray farr;
    cout << fapply.GetSum(farr, 3) << endl;

    return 0;
}

虽然可以得到结果,但是并不完美,因为所有的返回值和传入的参数类型都使用了 float。将 int 类型也强转为 float 类型了。我们无法返回 IntArray 或者 FloatArray 中处理器的数据的类型。因为 int 可以强转,侥幸使得程序能运行,但是如果是复杂的类型,就行不通了。

这个时候,就需要 traits 技术了,讲完了 traits 后,再回来看这个问题的解决方法。

traits 概念

Traits,又被叫做特性萃取技术,说得简单点就是提取“被传进的对象”对应的返回类型,让同一个接口实现对应的功能。因为STL的算法和容器是分离的,两者通过迭代器链接。算法的实现并不知道自己被传进来什么。萃取器相当于在接口和实现之间加一层封装,来隐藏一些细节并协助调用合适的方法,这需要一些技巧(例如,偏特化)。总的来说,Traits 的作用主要是用来为使用者提供类型信息。

trains 技术需要用到的技术有:模板特化及 typename 关键字等。

模板特化(Template Specialization)

模板特化分为模板全特化模板偏特化

类模板全特化:

有时为了需要,针对特定的类型,需要对模板进行特化,也就是特殊处理。例如 stack 类模板针对 bool类型。因为实际上 bool 类型只需要一个二进制位,就可以对其进行存储,使用一个字或者一个字节都是浪费存储空间的。

template <typename T>
class stack
{
    //...
};

template <>
class stack<bool>
{
    //...
};

类模板的偏特化:

模板的偏特化是指需要根据模板的某些但不是全部的参数进行特化。例如 C++ 标准库中的类 vector 的定义:

template <class T, class Allocator>
class vector
{
    //...
};

template <class Allocator>
class vector<bool, Allocator>
{
    //...
};

这个偏特化的例子中,一个参数被绑定到 bool 类型,而另一个参数仍未绑定需要由用户指定。

typename 关键字

先看一个问题,以下模板的声明中, class 和 typename 有什么不同?

template<class T> class Test;
template<typename T> class Test;

答案:没有不同。

然而,C++ 并不总是把 class 和 typename 视为等价。有时候我们一定得使用 typename。

默认情况下,C++ 语言假定通过作用域运算符访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。我们通过使用关键字 typename 来实现这一点:

template<typename T>
typename T::value_type top(const T &c)
{
    if (!c.empty())
        return c.back();
    else
        return typename T::value_type();
}

top 函数期待一个容器类型的实参,它使用 typename 指明其返回类型,并在 c 中没有元素时生成一个初始值的默认元素,并返回给调用者。

在这里我们只需要记住一点,当我们希望通知编译器一个名字表示类型时,必须使用关键字 typename,而不能使用 class。

实现 Traits Classes

完了背景知识,我们正式进入 traits 的关键地带。

我们知道,在 STL 中,容器与算法是分开的,彼此独立设计,容器与算法之间通过迭代器联系在一起。那么,算法是如何从迭代器类中萃取出容器元素的类型的?没错,这正是我们要说的 traits classes 的功能。

迭代器所指对象的类型,称为该迭代器的 value_type。我们来简单模拟一个迭代器 traits classes 的实现。

template <class IterT>
struct my_iterator_traits
{
    typedef typename IterT::value_type value_type;
};

my_iterator_traits 其实就是个类模板,其中包含一个类型的声明。有上面 typename 的基础,相信大家不难理解 typedef typename IterT::value_type value_type; 的含义:将迭代器的 value_type 通过 typedef 为 value_type。

对于 my_iterator_traits,我们还有一个一个偏特化版本。

template <class IterT>
struct my_iterator_traits<IterT *>
{
    typedef IterT value_type;
};

即如果 my_iterator_traits 的实参为指针类型时,直接使用指针所指元素类型作为 value_type

为了测试 my_iterator_traits 能否正确萃取迭代器元素的类型,我们先编写以下的测试函数。

void fun(int a) {
    cout << "fun(int) is called" << endl;
}

void fun(double a) {
    cout << "fun(double) is called" << endl;
}

void fun(char a) {
    cout << "fun(char) is called" << endl;
}

我们通过函数重载的方式,来测试元素的类型。

测试代码如下:

my_iterator_traits<vector<int>::iterator>::value_type a;
fun(a); // 输出 fun(int) is called

my_iterator_traits<vector<double>::iterator>::value_type b;
fun(b); // 输出 fun(double) is called

my_iterator_traits<char *>::value_type c;
fun(c); // 输出 fun(char) is called

为了便于理解,我们这里贴出 vector 迭代器声明代码的简化版本:

template <typename T, ...>
class vector
{
public:
    class iterator
    {
    public:
        typedef T value_type;
        //...
    };
    //...
};

我们来解释 my_iterator_traits<vector<int>::iterator>::value_type a; 语句的含义。

vector<int>::iteratorvector<int> 的迭代器,该迭代器包含了 value_type 的声明,由 vector 的代码可以知道该迭代器的value_type 即为 int 类型。

接着,my_iterator_traits<vector<int>::iterator> 会采用 my_iterator_traits 的通用版本,即 my_iterator_traits<vector<int>::iterator>::value_type 使用 typename IterT::value_type 这一类型声明,这里 IterT 为 vector<int>::iterator,故整个语句萃取出来的类型为 int 类型。

对 double 类型的 vector 迭代器的萃取也是类似的过程。

my_iterator_traits<char*>::value_type 则使用 my_iterator_traits 的偏特化版本,直接返回 char 类型。

由此看来,通过 my_iterator_traits ,我们正确萃取出了迭代器所指元素的类型。

总结一下我们设计并实现一个 traits class 的过程:

  1. 确认若干我们希望将来可取得的类型相关信息。例如,对于上面的迭代器,我们希望取得迭代器所指元素的类型。
  2. 为该信息选择一个名称。例如,上面我们起名为 value_type。
  3. 提供一个 template 和一组特化版本(例如,我们上面的 my_iterator_traits),内容包含我们希望支持的类型相关信息。

问题解决

通过上面对 traits 的描述,现在用 traits 解决最初遇到的问题。

#include <iostream>
using namespace std;

//处理int类型
class IntArray
{
public:
    IntArray()
    {
        a = new int[10];
        for (int i = 0; i < 10; ++i)
        {
            a[i] = i + 1;
        }
    }
    ~IntArray()
    {
        delete[] a;
    }

    int GetSum(int times)
    {
        int sum = 0;
        for (int i = 0; i < 10; ++i)
            sum += a[i];
        cout << "int sum=" << sum << endl;
        return sum * times;
    }

private:
    int *a;
};

//处理float类型
class FloatArray
{
public:
    FloatArray()
    {
        f = new float[10];
        for (int i = 1; i <= 10; ++i)
        {
            f[i - 1] = 1.0f / i;
        }
    }
    ~FloatArray()
    {
        delete[] f;
    }
    float GetSum(float times)
    {
        float sum = 0.0f;
        for (int i = 0; i < 10; i++)
            sum += f[i];
        cout << "float sum=" << sum << endl;
        return sum * times;
    }

private:
    float *f;
};

//基础模板
template <class T>
class NumTraits
{
};

//模板特化
template <>
class NumTraits<IntArray>
{
public:
    typedef int resulttype;
    typedef int inputargtype;
};

template <>
class NumTraits<FloatArray>
{
public:
    typedef float resulttype;
    typedef float inputargtype;
};

template <class T>
class Apply2
{
public:
    typename NumTraits<T>::resulttype GetSum(T &obj, typename NumTraits<T>::inputargtype inputarg)
    {
        return obj.GetSum(inputarg);
    }
};

int main()
{
    IntArray iarr;
    FloatArray farr;

    Apply2<IntArray> ai2;
    Apply2<FloatArray> af2;

    cout << "2整型数组的和3倍:" << ai2.GetSum(iarr, 3) << endl;
    cout << "2浮点数组的和3.2倍:" << af2.GetSum(farr, 3.2f) << endl;

    return 0;
    return 0;
}

得到的结果如下:

$ ./test 
2整型数组的和3倍:int sum=55
165
2浮点数组的和3.2倍:float sum=2.92897
9.3727

可以看到,我们使用了一个额外的 NumTraits 的类型萃取器,通过这个萃取器我们便可以得到对应的类型的返回值类型和传入值类型,完美的解决了问题。

参考:
细说 C++ Traits Classes

相关标签: C++