什么是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++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

在 VSCode 中配置 C++ 开发环境的详细教程

《在VSCode中配置C++开发环境的详细教程》本文详细介绍了如何在VisualStudioCode(VSCode)中配置C++开发环境,包括安装必要的工具、配置编译器、设置调试环境等步骤,通... 目录如何在 VSCode 中配置 C++ 开发环境:详细教程1. 什么是 VSCode?2. 安装 VSCo

C++11的函数包装器std::function使用示例

《C++11的函数包装器std::function使用示例》C++11引入的std::function是最常用的函数包装器,它可以存储任何可调用对象并提供统一的调用接口,以下是关于函数包装器的详细讲解... 目录一、std::function 的基本用法1. 基本语法二、如何使用 std::function

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝