首页 C++ STL - 2 - 迭代器设计思路。萃取。
文章
取消

C++ STL - 2 - 迭代器设计思路。萃取。

C++ STL - 2 - 迭代器设计思路。萃取。

什么是萃取?

三个字:中间层

我们这里先以侯捷老师的iterator_traits做为例子。

我们知道,iterator迭代器本身有五个属性。我们也知道迭代器是一个class。所以也就是迭代器类有五个typedef。分别是:

1
2
3
4
5
6
7
8
9
10
11
12
namespace std
{
    template <typename T>
    struct iterator_traits
    {
        typedef typename T::iterator_category   iterator_category;  //迭代器分类,比如正向迭代器,双向迭代器
        typedef typename T::value_type          value_type;  //迭代器指向的对应的数据类型,比如int string 等等
        typedef typename T::difference_type     difference_type; //用来表示两个迭代器之间的距离,因此也可以用来表示一个容器的最大容量
        typedef typename T::pointer             pointer; 
        typedef typename T::reference           reference; 
    };
}

算法组想通过迭代器访问容器数据。假设我们的算法组就一定要问你,迭代器指向的数据的类型是什么,我们可以轻易地像这样获取:

1
list<int>::iterator::value_type

它会回答你是一个int

这样非常好。但是有一个问题。迭代器是一个泛化的指针。反过来说,指针就是一个退化的迭代器。

我们发现在迭代器萃取类内部有一大堆的typedef。我们正是通过这个来告诉外面你这个迭代器的value_type是个什么玩意。

举个小例子:

1
2
3
4
5
6
7
8
9
class test{
    public:
    test(){}
    typedef int inputtype;
};

int main(){
    test::inputtype x; //这句话等于告诉你 test类下面的inputtype这个东西是int 
}

这里不需要加typename的原因是这不是模板类。类型是已经确定好的。后文会讲到。

但是如果我们给算法传入的不是迭代器,而是一个指针怎么办?指针不是类,指针不是结构体,指针无法给自己弄一堆typedef。也就是你问一个指针,问他你的value_type是什么,他懵逼了,说我没有这玩意啊?怎么办

这就是萃取的意义。我们可以间接地获取类型。也就是利用模板的特化包装一层,让算法统一去问萃取层,这个东西的value_type是个什么玩意。

QQ截图20220613172835

在图里,我们可以看见,如果传入的是迭代器,我们可以走到1里面,直接问迭代器的value_type是什么。

如果传入的是一个指针,我们可以走到2里面。人为地设定一个value_type。也就是把T提取出来。告诉算法组这个指向T类型的指针的value_typeT类型

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<typename T>
class pclass{
public:
    typedef T valuetype;
};

template<typename T>
class pclass<T*>{
public:
    typedef T valuetype;
};

int main(){
    pclass<int>::valuetype x; //x的类型是int
    pclass<int*>::valuetype x; //x的类型还是int。 可以看成T* = int*, 拿掉*就是 T = int 所以还是int。
    return 0;
}

整体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
//基本类

class FLOATtype{
public:
    float _val;
    FLOATtype(){}
    FLOATtype(float x):_val(x){};
    float retval (float& num){
        cout << _val*num << endl;
        return _val*num;
    }
    //我们假设FLOATtype没有自己的获取输入输出类型的部分
};

class INTtype{
public:
    int _val;
    INTtype(){}
    INTtype(int x):_val(x){};
    int retval (int& num){
        cout << _val*num << endl;
        return _val*num;
    }
    //我们假设INTtype没有自己的获取输入输出类型的部分
};


class fucktype{
public:
    int _val;
    fucktype(){}
    fucktype(int x):_val(x){};
    int retval (int& num){
        cout << _val*num << endl;
        return _val*num;
    }
    typedef fucktype rettype; //我们假设fucktype有自己的获取输入输出类型的部分。所以我们加了typedef。
    typedef int inputtype;
};




//萃取 模板特化
/*
这里的例子看起来非常脱裤子放屁。但是对于迭代器萃取来讲完全不是。
我们知道迭代器是泛化的指针,也就是说指针是退化的迭代器。
我们知道迭代器是一个class 所以他们有能力定义自己的associate type。
举个例子。我们知道迭代器类里面有自己的value_type。所以我们可以直接问 list<int>::iterator你的valuetype是什么,像这样 list<int>::iterator::value_type。它会回答你它是int。
但是如果我们给算法传入的不是迭代器,而是一个指针怎么办?指针不是类,指针不是结构体,指针无法给自己弄一堆typedef。怎么办
这就是萃取的意义。我们可以间接问。就是放入萃取机。
*/
template <typename T>
class testTraits{ //默认版本。直接问对应类的输入输出类型。如果有的话
    public:
        typedef typename T::rettype rettype; //注意这里一定要加typename告诉编译器 T::rettype是一个类型。
        typedef typename T::inputtype inputtype; //注意这里一定要加typename告诉编译器 T::rettype是一个类型。告诉编译器T类下的inputtype代表的不是变量,代表的是类型。
};

template<> //模板全特化。我们假设知道inttype不是类,不能放typename 就好比指针。所以我们在这里给他包一层,显式告知对应的输入输出类型是什么。让有人问的时候转到这里告诉他对应类型
class testTraits<INTtype>{
    public:
        typedef int rettype;
        typedef int inputtype;
};

template<>
class testTraits<FLOATtype>{ //和上面一样。
    public:
        typedef float rettype;
        typedef float inputtype;
};

//使用

/*
在这个例子里面。我们对照着迭代器萃取器的例子做一个总结。
假如我们有几个类是有inputtype的。到时候直接问那个类就可以。但是如果是一个普通变量或者指针,没有inputtype的怎么办?
就好比我们的fucktype。我们在fucktype类里面有自己的typedef。所以可以直接问。
但是INTtype和FLOATtype没有,不能直接问。我们就通过模板特化来加一个中间层。所以萃取其实就是中间层。
*/

template<typename T>
class test{
    public:
    //注意这里testTraits<T>::rettype是做为函数的返回类型。所以必须要加typename。不然编译器会认为rettype是一个在testTraits里的成员变量。但是因为是把testTraits类里的rettype成员变量对应的typedef的东西当做类型来看待。所以需要加typename
    typename testTraits<T>::rettype retvall(T& obj, typename testTraits<T>::inputtype input){
        return obj.retval(input);
    }
};

int main(){
    INTtype intnum(5);
    int INTarg = 5;
    test<INTtype> test1;
    test1.retvall(intnum, INTarg);

    FLOATtype floatnum(5.5f);
    float FLOATarg = 8.8f;
    test<FLOATtype> test2;
    test2.retvall(floatnum, FLOATarg);

    fucktype fucknum(10);
    int fuckarg = 5;
    test<fucktype> test3;
    test3.retvall(fucknum, fuckarg);
}

类作用域

在类外部访问类中的名称时,可以使用类作用域操作符,形如MyClass::name的调用通常存在三种:静态数据成员、静态成员函数和嵌套类型:

1
2
3
4
5
struct MyClass {
    static int A;
    static int B();
    typedef int C; //这里就是typedef 也就是嵌套类型
}

MyClass::A, MyClass::B, MyClass::C分别对应着上面三种。

typedef 和作用域解析运算符:: 和 嵌套类

我们很难把class和namespace联系起来,但是这两个在抽象层次上其实是一个概念。

作用域解析运算符::的作用就是制定某一个范围。但是一旦用在了嵌套类或者类内的typedef,我们就很难理解这层含义。

我们都知道直接访问类静态成员必须使用作用域解析运算符::

但是我们如果要通过作用域解析运算符访问非静态成员,可以吗?当然可以。只不过需要通过对象访问。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class test{
    public:
    class testinner{ //嵌套类
        public:
        int _val;
        testinner(){}
        testinner(int x):_val(x){}
    };
    test(){}
    typedef int testdef;


    int s;
};

int main()
{
    test::testinner tt1 = test::testinner(5); //OK
    test::testinner tt2; //OK 访问嵌套类
    test::testdef tt3 = 5; //OK 访问typedef。
    test::s = 5; //不行。直接访问的成员必须是静态成员。
    test t;
    t.test::s = 5; //OK 通过对象访问,但是还是脱裤子放屁加了作用域解析运算符。

    return 0;
}

这里我们可以理解为我们需要访问的typedef和嵌套类在test类的命名空间下。我们必须要告知编译器这东西在哪,所以需要通过作用域解析运算符去访问。

至于typedef和嵌套类是否是一个类的成员,我没有查到确切的说法。如果说他们不是成员吧,但是有成员的属性。要是说是成员吧,我们也可以直接访问。但是也有人说嵌套类的static是隐式的。也有人把嵌套类当做一个namespace 来看待。所以我的理解是不要把嵌套类和typedef看做类成员。假设他们可以直接调用即可。

如果嵌套类没有在外部类中实例化,则实例化外部类的时候不会实例化嵌套类内容。

最后说一下必须显式使用typename的情况。

QQ截图20220613134505

来几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class test{
    public:
        struct obj{
            int _sb;
        };
        obj s;
        int _val;
        test(){}
        test(int x):_val(x){}
        test(int x, int y){
            _val = x;
            s._sb = y;
        }
};
template<typename T>
void func(){
    test t(5,8);
    typename T::obj* ptr = &t.s;
    //这里我们的意思是有一个指针ptr指向了T类里面的obj类型的对象。翻译成人话也就是ptr是一个T::obj类型的指针。但是如果有一个T类里面的obj是一个变量,如static int obj = 8
    //那么这就变成了变量乘法。会有歧义。所以使用typedef显式告知编译器 T::obj不是一个变量,而是一个类型。
    cout << ptr->_sb << endl;
} 

可能还不够?再来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef iterator_traits<T>::value_type value_type_anothername; 
//这句话的意思是value_type_anothername是在模板类iterator_traits中的 变量 value_type的别名。

//所以如果有
value_type_anothername name = "foward_iterator";
//这样是不行的。因为value_type_anothername是一个变量不是类型。
//所以我们需要加typedef告知编译器这个是类型

typedef typename iterator_traits<T>::value_type value_type_anothername; 
//这句话的意思是value_type_anothername是iterator_traits<T>::value_type这个 类型 的别名。

//所以这样可以有
value_type_anothername name = "foward_iterator";
//因为value_type_anothername 是个类型。就好比int string这种。

还不够?再来:

这是我们前文的例子。但是为什么这里不需要加typedef呢?

1
2
3
4
5
6
7
8
9
class test{
    public:
    typedef int inputtype;
};

int main(){
    test::inputtype x = 4; //这句话等于告诉你 test类下面的inputtype这个东西是int 
}

先送上大佬文章一篇:https://feihu.me/blog/2014/the-origin-and-usage-of-typename/

由于test已经是一个完整的定义,因此编译期它的类型就可以确定下来,也就是说test::inputtype这些名称对于编译器来说也是已知的。

可是,如果是像T::inputtype这样呢?T是模板中的类型参数,它只有等到模板实例化时才会知道是哪种类型,更不用说内部的inputtype。通过前面类作用域一节的介绍,我们可以知道,T::inputtype实际上可以是以下三种中的任何一种类型:

  • 静态数据成员
  • 静态成员函数
  • 嵌套类型

  • typename的作用,简单理解就是强制告诉编译器 namespace::objname这个东西是一个类型名而不是变量名。

所以在模板类中,如果想要告知编译器一个使用了::作用域解析运算符的东西是类型,而不是变量,就需要加typename

  • 如果直接把这个东西当做一个类型来进行变量的声明,那就不需要搭配typedef

  • 如果需要把这个东西当做一个类型来赋予一个别名,那就需要使用typedef

所以,像这样就必须要加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class test{
    public:
    test(){}
    typedef int inputtype;
};


template<typename T>
class test1{
    public:
    test1(){};
    typedef typename T::inputtype inputtype; //注意这里,T::inputtype是个类型。比如test::inputtype 就是 int类型
};

限定名、非限定名

限定名(qualified name),故名思义,是限定了命名空间的名称。看下面这段代码,coutendl就是限定名:

1
2
3
4
5
#include <iostream>

int main()  {
    std::cout << "Hello world!" << std::endl;
}

coutendl前面都有std::,它限定了std这个命名空间,因此称其为限定名。

如果在上面这段代码中,前面用using std::cout;或者using namespace std;,然后使用时只用coutendl,它们的前面不再有空间限定std::,所以此时的coutendl就叫做非限定名(unqualified name)。

依赖名、非依赖名

依赖名(dependent name)是指依赖于模板参数的名称,而非依赖名(non-dependent name)则相反,指不依赖于模板参数的名称。看下面这段代码:

1
2
3
4
5
6
7
8
9
10
template <class T>
class MyClass {
    int i;
    vector<int> vi;
    vector<int>::iterator vitr;

    T t;
    vector<T> vt;
    vector<T>::iterator viter;
};

因为是内置类型,所以类中前三个定义的类型在声明这个模板类时就已知。然而对于接下来的三行定义,只有在模板实例化时才能知道它们的类型,因为它们都依赖于模板参数T。因此,T, vector<T>vector<T>::iterator称为依赖名。前三个定义叫做非依赖名。

更为复杂一点,如果用了typedef T U; U u;,虽然T没再出现,但是U仍然是依赖名。由此可见,不管是直接还是间接,只要依赖于模板参数,该名称就是依赖名。

本文由作者按照 CC BY 4.0 进行授权