什么是C++ traits?

2023-12-12 09:48
文章标签 c++ traits

本文主要是介绍什么是C++ traits?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

今年网易最后一道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的算法多态性选择是在编译时决定的,比起在运行时决定效率更好。

奋斗奋斗奋斗奋斗


 

这篇关于什么是C++ traits?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/484156

相关文章

C#如何调用C++库

《C#如何调用C++库》:本文主要介绍C#如何调用C++库方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录方法一:使用P/Invoke1. 导出C++函数2. 定义P/Invoke签名3. 调用C++函数方法二:使用C++/CLI作为桥接1. 创建C++/CL

C++如何通过Qt反射机制实现数据类序列化

《C++如何通过Qt反射机制实现数据类序列化》在C++工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作,所以本文就来聊聊C++如何通过Qt反射机制实现数据类序列化吧... 目录设计预期设计思路代码实现使用方法在 C++ 工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作。由于数据类

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

C++中初始化二维数组的几种常见方法

《C++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

C++ vector的常见用法超详细讲解

《C++vector的常见用法超详细讲解》:本文主要介绍C++vector的常见用法,包括C++中vector容器的定义、初始化方法、访问元素、常用函数及其时间复杂度,通过代码介绍的非常详细,... 目录1、vector的定义2、vector常用初始化方法1、使编程用花括号直接赋值2、使用圆括号赋值3、ve

如何高效移除C++关联容器中的元素

《如何高效移除C++关联容器中的元素》关联容器和顺序容器有着很大不同,关联容器中的元素是按照关键字来保存和访问的,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的,本文介绍了如何高效移除C+... 目录一、简介二、移除给定位置的元素三、移除与特定键值等价的元素四、移除满足特android定条件的元

Python获取C++中返回的char*字段的两种思路

《Python获取C++中返回的char*字段的两种思路》有时候需要获取C++函数中返回来的不定长的char*字符串,本文小编为大家找到了两种解决问题的思路,感兴趣的小伙伴可以跟随小编一起学习一下... 有时候需要获取C++函数中返回来的不定长的char*字符串,目前我找到两种解决问题的思路,具体实现如下:

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++