首页 > 代码库 > C++模板 - traits

C++模板 - traits

traits是个很好玩的东西,在泛型编程里面很常见。最早出于老外的一篇论文。http://www.cantrip.org/traits.html?seenIEPage=1 建议仔细阅读。

 

首先我们来看一段代码。

template<class T>
T accum(const T* ptr, int len)
{
	T total = T();

	for (int i = 0; i < len; i++)
	{
		total += *(ptr + i);
	}

	return total;
}


这是个模板函数,很简单,就是把传进来的参数累加,返回结果。

那么这段代码有什么问题呢?

我们写几行代码来测试一下:

	int sz[] = {1, 2, 3, 4, 5};
	int v1 = accum(&sz[0], 5) / 5;

	char str[] = {'a', 'b', 'c', 'd', 'e'};
	char v2 = accum(&str[0], 5) / 5;

前面两行代码把1,2,3,4,5累加然后去平均值。后面两行代码把abcde累计,然后取平均值。

运行结果如下:

v1是正确的,v2是错的(正确的应该是‘c‘。

那为什么传进去int类型的时候是对的,但是传入char是错的呢?

template<class T>
T accum(const T* ptr, int len)
{
	T total = T();  

	for (int i = 0; i < len; i++)
	{
		total += *(ptr + i);
	}

	return total;
}

我们再来看一下这个累加模板函数,注意T total = T();这一行。当const T* ptr指向int数组时,T total就是个int类型。那么12345加起来是可以存放到int类型变量里面的。
但是当ptr指向char数组时,total就是char类型,char只有一个字节8位的长度。除去符号位,一个char用来存放数字的时候就只有7位了,2的7次方 = 128,也就是最大值是127.而abcde加起来有495,所有就越界了。累加函数返回的时候就会把数据截断。结果就得到一个-3的错误平均值了。

OK, 现在我们知道这个问题的关键在于存放结果的数据类型太小了。

那么如果解决这个问题呢?

有一个简单办法

办法一:增加一个模板来指定返回值。

既然问题的关键是返回值类型太小,那么首先能想到的就是指定返回类型了。比如:

template<class T, class AccuT>
AccuT accum2(const T* ptr, int len)
{
	AccuT total = AccuT();

	for (int i = 0; i < len; i++)
	{
		total += *(ptr + i);
	}

	return total;
}

增加一个模板参数,然后调用的时候指定返回值类型,如:

char v3 = accum2<char, int>(&str[0], 5) / 5;

这样就可以得到正确的结果了。这确实是个办法,但是不是很好,调用者每次都要指定返回值,很麻烦而且还有弄错的情况。接下来就介绍traits的方案。

 

办法二:traits

我们可以更一般的来考虑一下这个问题,是不是说每种传进来的参数都需要有一个对应的存放结果的类型呢?

如果每次传进来的参数都有一个对应的返回类型,那么这个问题应该就解决了。

我们考虑增加一个模板。

template<typename T>
struct traits;

这个模板啥都没。然后使用模板的特化,

template<>
struct traits<char>
{
	typedef int AccuT;
};

针对char来一个特化。

再来修改一下累加函数:

template<class T>
typename traits<T>::AccuT accum3(const T* ptr, int len)
{
	traits<T>::AccuT total = traits<T>::AccuT();

	for (int i = 0; i < len; i++)
	{
		total += *(ptr + i);
	}

	return total;
}

把存放结果的类型改成traits<T>::AccuT。这个时候再来跑一下累加abcde:

char v4 = accum3(&str[0], 5) / 5;

可以得到正确的结果‘c‘了。

仔细看一下accum3,感觉好像是从类型T里面得到返回值类型。也可以理解为返回值类型是T的一个特性。这大概也就是traits这个名字的来源吧。

那如果传入一个int数组呢?

char v4 = accum3(&sz[0], 5) / 5;

编译器直接报错。那是因为没有int的traits。给int加个traits就可以了:

template<>
struct traits<int>
{
	typedef int AccuT;
};

那如果再传入一个float数组呢?那编译器又报错了,因为没有float traits。这也是traits的一个好处,如果没有定义相应的traits,那么编译器直接报错,这也可以更早的发现问题。跟办法一比较,也不需要调用者自己指定返回类型了,减少了出错的可能性。还有比如传入的int数值累加值超过了int的最大值,那么直接修改int traits就行了,比如改成:

template<>
struct traits<int>
{
	typedef long long AccuT;
};

其他地方都不需要修改。

总体来说traits还是很有用的,如果当碰到需要给某个类型指定一个对应类型的时候,往往可以考虑traits。STL里面的迭代器也有用到traits。

 完整代码:

// ConsoleApplication1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <memory>

#include <Windows.h>
#include <TlHelp32.h>

#include <functional>


template<class T>
T accum(const T* ptr, int len)
{
	T total = T();  

	for (int i = 0; i < len; i++)
	{
		total += *(ptr + i);
	}

	return total;
}

/*****************************附加一个返回类型**********************************************/
template<class T, class AccuT>
AccuT accum2(const T* ptr, int len)
{
	AccuT total = AccuT();

	for (int i = 0; i < len; i++)
	{
		total += *(ptr + i);
	}

	return total;
}

/*****************************traits**********************************************/
template<typename T>
struct traits;

template<>
struct traits<char>
{
	typedef int AccuT;
};

template<>
struct traits<int>
{
	typedef int AccuT;
};

template<class T>
typename traits<T>::AccuT accum3(const T* ptr, int len)
{
	traits<T>::AccuT total = traits<T>::AccuT();

	for (int i = 0; i < len; i++)
	{
		total += *(ptr + i);
	}

	return total;
}


int _tmain(int argc, _TCHAR* argv[])
{
	char tt = 128;
	int bb = tt;

	int sz[] = {1, 2, 3, 4, 5};
	int v1 = accum(&sz[0], 5) / 5;

	char str[] = {'a', 'b', 'c', 'd', 'e'};
	char v2 = accum(&str[0], 5) / 5;

	char v3 = accum2<char, int>(&str[0], 5) / 5;

	char v4 = accum3(&str[0], 1) / 1;

	return 0;
}


 

 

 

 

C++模板 - traits