什么是C++ traits?

来源:互联网 发布:网上怎么自学淘宝 编辑:程序博客网 时间:2024/06/06 14:07
今年网易最后一道C++笔试题是考了这样一道题目:C++的traits是什么机制,有什么用?请举例说明。

    我没答上来,回来查了一下,才发现是和STL泛化编程相关的。从网上找来两篇候捷的大作一读,才有点明白。现在写下来,看我是否真的理解了。首先,我们来了解一下什么是泛化编程。

      一般泛型编程时,比如我设计一个算法:

template<class I, class T>
I find(I first, I end, T& value)
{
   while( first != end && *first != value) //需要重载iterator间的“!= *提领”算子,重载T间的比较算子
           first++;//需要重载后置式++算子
   return first;
}

first,end是class,一般就是iterator,而class T就是iterator所指之物的类型;在这个模范函数里,我们声明了两个类型I,T。事实上,I与T是相关的,比如int*与int。比如我有一个

struct node
{
   int val;
   node *pnext;
};

在上面需要运用find算法,就需要一个iterator包装,在这里我申明一个类模板:

template<class T>
struct Node{//C++中struct与class的区别在于struct中members默认access level是public,class是private
      T *ptr;
     Node(const T* p):ptr(p){}
     T& operator*() const { return *ptr; }//重载*提领算子,返回的是T类型
     T* operator->() const { return ptr; }
     Node& operator++{ ptr = ptr->pnext; return *this; }//前置式++,返回的是引用
     Node operator++(int) { Node t = *this; ++*this; return t; }//后置式++,因ptr已经改变,返回的不是引用
     bool operator==(const Node& i){ return i.ptr == ptr; }
     bool operator!= (const Node& i){ return i.ptr != ptr; }//同样为了find函数中而重载!=符号
};

同样,我们在*first != value之间,我们需要重载!=算子(在find函数中是*first与value比较,而*first是T类型,这里T类型就是struct node类型):
bool operator==(const node& i, int value){ return i.value == value; }
bool operator!= (const node& i, int value){ return i.value != value; }

好了,现在我们可以使用以下代码使用我们的链表:
node *head,*end;
node *tmp = new node;
tmp->value = 100;
tmp->pnext = NULL;
head = end = tmp;

for(int i = 0; i < 10; ++i)
{
tmp = new node;
tmp->value = i+1;
tmp->pnext = NULL;
end->pnext = tmp;
end = tmp;
}
//以上代码生成了一个链表,现在看怎么运用我们的find函数:
Node<node> r;
r = find(Node<node>(head), Node<node>(), 5);
//Node<node>(head)调用Node<node>构造函数,入参是head
//同理,Node<node>()的入参是NULL
if( NULL != r ) cout<<(*r).value<<endl; //如果r不是NULL,就输出

到这里,我们学会了如何封装一个struct,使其能被find函数调用,很有成就感吧?感谢jjh吧。

我们重新审视find函数,发现find函数需要声明两个类型,一个是T,一个是I,其实T就是的*I,C++没有typeof算子,但是编译器有推导功能:

办法一:
template<class I,class T>
void fun_impl(I i, T v)
{
   //do some work
}

template<class I>
void fun(I i)
{
fun_impl(i, *i);//编译器通过*i推导出*i的类型,然后调用fun_impl完成功能
}
于是我们可以通过如下代码完成功能:
int i;
fun(&i);

     似乎解决了问题,但是问题不断,如果入参不是一般参数,而是一个函数的传回值,就不灵了。

方法二(嵌套类型声明,原文称“巢狀式的型別宣告”):
假设我们的Node模板类封装了类型为T节点
template<class T>
struct Node{
     typedef T value_type;//嵌套类型
      T *ptr;
     Node(const T* p):ptr(p){}
     T& operator*() const { return *ptr; }
     T* operator->() const { return ptr; }
    .....
};

那泛化函数可以如此声明:
template<class T>
typename Node<T>::value_type
func(Node<T>&it)//传入一个iterator
{
return *(it.ptr);
}

然后我们可以用下面的代码:
Node<int>ite(new int(100));
cout<<func(ite)<<endl;

这个函数的问题在于每个需要为每一种iterator写一个func,Node写一个,以后Stack也许也要写一个,有没有办法可以避免具体类型Node之类出现呢?当然有了(traits粉墨登场),traits是特性的意思,从众多iterator中“提取”type特性:

template<class Iterator>
struct iterator_traits
{
typedef typename Iterator::value_type value_type;//typename为了使编译通过,其实g++ 3.4.2下不会报错
};


至于原生指针,我们使用partial specialization
template<class T>
struct iterator_traits<T*>
{
typedef T value_type;
}
template<class T>
struct iterator_traits<const T*>
{
typedef T value_type;
}

于是乎,func函数可以写成如下:
template<class T>
typename iterator_traits<T>::value_type
func(T t)
{
return *(t.value);
}

//测试
int main( char argc, char *argv[] )
{
char *p[100];

Node<int>ite (new int(100));
std::cout<<func(ite)<<"\n";

Node<char>cite(new char('a'));
std::cout<<func(cite)<<"\n";

Node<char*>pstr(p);
 
return 0;

 

 

 

 

Traits技术可以用来获得一个 类型 的相关信息的。 首先假如有以下一个泛型的迭代器类,其中类型参数 T 为迭代器所指向的类型:

template
<typename T>
class myIterator
{
...
};

当我们使用myIterator时,怎样才能获知它所指向的元素的类型呢?我们可以为这个类加入一个内嵌类型,像这样:
template <typename T>
class myIterator
{
typedef T value_type;
...
};
这样当我们使用myIterator类型时,可以通过 myIterator::value_type来获得相应的myIterator所指向的类型。

现在我们来设计一个算法,使用这个信息。
template <typename T>
typename
myIterator<T>::value_type Foo(myIterator<T> i)
{
...
}
这里我们定义了一个函数Foo,它的返回为为 参数i 所指向的类型,也就是T,那么我们为什么还要兴师动众的使用那个value_type呢? 那是因为,当我们希望修改Foo函数,使它能够适应所有类型的迭代器时,我们可以这样写:
template <typename I>//这里的I可以是任意类型的迭代器
typename I::value_type Foo(I i)
{
...
}
现在,任意定义了 value_type内嵌类型的迭代器都可以做为Foo的参数了,并且Foo的返回值的类型将与相应迭代器所指的元素的类型一致。至此一切问题似乎都已解决,我们并没有使用任何特殊的技术。然而当考虑到以下情况时,新的问题便显现出来了:

原生指针也完全可以做为迭代器来使用,然而我们显然没有办法为原生指针添加一个value_type的内嵌类型,如此一来我们的Foo()函数就不能适用原生指针了,这不能不说是一大缺憾。那么有什么办法可以解决这个问题呢? 此时便是我们的主角:类型信息榨取机 Traits 登场的时候了

....drum roll......

我们可以不直接使用myIterator的value_type,而是通过另一个类来把这个信息提取出来:
template <typename T>
class Traits
{
typedef typename T::value_type value_type;
};
这样,我们可以通过 Traits<myIterator>::value_type 来获得myIterator的value_type,于是我们把Foo函数改写成:
template <typename I>//这里的I可以是任意类型的迭代器
typename Traits<I>::value_type Foo(I i)
{
...
}
然而,即使这样,那个原生指针的问题仍然没有解决,因为Trait类一样没办法获得原生指针的相关信息。于是我们祭出C++的又一件利器--偏特化(partial specialization):
template <typename T>
class Traits<T*> //注意 这里针对原生指针进行了偏特化
{
typedef typename T value_type;
};
通过上面这个 Traits的偏特化版本,我们陈述了这样一个事实:一个 T* 类型的指针所指向的元素的类型为 T。

如此一来,我们的 Foo函数就完全可以适用于原生指针了。比如:
int * p;
....
int i = Foo(p);
Traits会自动推导出 p 所指元素的类型为 int,从而Foo正确返回。

 

 

 

 

 

 

 

 

 

 

 

 

 

《STL源码解析》是侯杰大师翻译的著作,其中在Iterator一章着重介绍了traits技巧,认为traits技巧是搞懂的STL源码的入门钥匙,既然编写STL的神人们都这么重视traits,那么traits到底能帮助我们解决什么问题呢?traits的作用在于能“提取”出类型的特性。

举个例子:有个需求是这样的,需要写一个全局的print函数,来打印入参的对象,假设用OO的思想:设计一个cprint的基类,在此基类中用虚函数print,然后每个类型都继承cprint基类,并重写print,那么全局函数就可以这么写:

void print(const cprint& _p)

{

    _p.print();

}

很完美吧?哈哈,自己都看着得意,那么问题来了我需要print的类型是原生指针怎么办呢,OO的思想可以实现,但是需要写个包装类,试着换个角度看这个问题吧,来用traits技巧来解决看看:

首先声明两个结构体,没有任何东西,只为了标志,我们的程序世界就靠它们来为我们区分谁有print,谁木有print了。

struct _type_true {};

struct _type_false {};

//接下来这个是一个测试类,其有print函数。

struct student

{

    unsigned intid;

    unsigned intage;

    char name[128];

    void print(void) const

    {

        std::cout << "id:"<< id << "\t"<< "age:" << age << "\t"<< "name:" << name << std::endl;

    };

};

// OK,下面最厉害的traits就要出场了,默认的用_type_true来标记,再用偏特化(partial specialization)的方法声明原生指针是_type_false,木有print的

template<typename T>

struct print_traits

{

    typedef _type_truehas_print;

};

template<>

struct print_traits <int>

{

    typedef _type_falsehas_print;

};

//接下来就是全局的print函数了:通过print_traits的提取,区别调用_print(T *_p, _type_true)和_print(T *_p, _type_false)。

template<typename T>

void print(T _P)

{

    typedef typenameprint_traits<T>::has_print has_print;

    _print(_P,has_print());

}

template <typename T>

inline void _print(T _P, _type_true)

{

    _P.print();

}

template <typename T>

inline void _print(T _P, _type_false)

{

    std::cout<< "我是没有Print的,最后尝试下<<操作符吧:" << _P << std::endl;

}

//测试下哈

int main(int argc,char* argv[])

{

    student s1;

    s1.id= 0;

    s1.age= 19;

    strncpy(s1.name,"xia kan",sizeof(s1.name));

    print(s1);

    int i = 0;

    print(i);

    return 0;

}

总结下traits的运用方法:

声明标记-》运用标记,区分类别(traits)-》设计接口函数和不同类型的重载函数-》利用编译器的调用判定来决定调用哪个函数

traits是编译期多态的一个好应用~!从网上摘了这段话,很能说明问题:

“ traits技巧对类型做了什么?有什么作用?类型和类型的特性本是耦合在一起,通过traits技巧就可以将两者解耦。从某种意思上说traits方法也是对类型的特性做了泛化的工作,通过traits提供的类型特性是泛化的类型特性。从算法使用traits角度看,使用某一泛型类型的算法不必关注具体的类型特性(关注的是泛化的类型特性,即通过traits提供的类型特性)就可以做出正确的算法过程操作;从可扩展角度看,增加或修改新的类型不影响其它的代码,但如果是在type_traits类中增加或修改类型特性对其它代码有极大的影响;从效率方法看,使用type_traits的算法多态性选择是在编译时决定的,比起在运行时决定效率更好。

奋斗奋斗奋斗奋斗


 

原创粉丝点击