Wednesday, April 4, 2007

C++ Generic Programming: Traits: The else-if-then of Types


http://www.jjhou.com/myan-type-traits.htm


麽是traits,为什麽人们把它认为是 C++ Generic Programming 的重要技术?
简短截说,traits如此重要,是因为此项技术允许系统在编译时根据类型作一些决断,
就好像在运行时根据值来作出决断一样。更进一步,此技术遵循"另增一个间接层"
的谚语,解决了不少软件工程问题,traits使您能根据其产生的背景(context)
来作出抉择。这样最终的代码就变得清晰易读,容易维护。如果你正确运用了traits
技术,你就能在不付出任何性能和安全代价的同时得到这些好处,或者能够契合其他
解决方案上的需求。
例子:Traits不仅是泛型程序设计的核心工具,而且我希望以下的例子能够使你相信,
在非常特定的问题中,它也是很有用的。
假设你现在正在编写一个关系数据库应用程序。可能您一开始用数据库供应商提供的
API库来进行反问数据库的操作。但是理所当然的,不久之後你会感到不得不写一些
包装函数来组织那些原始的API,一方面是为了简洁,另一方面也可以更好地适应
你手上的任务。这就是生活的乐趣所在,不是吗?
一个典型的API是这样的:提供一个基本的方法用来把游标(cursor, 一个行集和或
者查询结果)处的原始数据传送到内存中。现在我们来写一个高级的函数,用来把某
一列的值取出来,同时避免暴露底层的细节。这个函数可能会是这个样子:
(假想的DB API用db或DB开头)
// Example 1: Wrapping a raw cursor int fetch
// operation.
// Fetch an integer from the
// cursor "cr"
// at column "col"
// in the value "val"
void FetchIntField(db_cursor& cr,
unsigned int col, int& val)
{
// Verify type match
if (cr.column_type[col] != DB_INTEGER)
throw std::runtime_error(
"Column type mismatch");
// Do the fetch
db_integer temp;
if (!db_access_column(&cr, col))
throw std::runtime_error(
"Cannot transfer data");
memcpy(&temp, cr.column_data[col],
sizeof(temp));
// Required by the DB API for cleanup
db_release_column(&cr, col);
// Convert from the database native type to int
val = static_cast<int>(temp);
}
这种接口函数我们所有人都可能不得不在某个时候写上一遍,它不好对付但又非常重
要,处理了大量细节,而且这还只是一个简单的例子。FetchIntField抽象,提供了
高一层次的功能,它能够从游标处取得一个整数,不必再担心那些纷繁的细节。
既然这个函数如此有用,我们当然希望尽可能重用它。但是怎麽做?一个很重要的泛化
步骤就是让这个函数能够处理int之外的类型。为了做到这一点,我们得仔细考虑代码中
跟int类型相关的部份。但首先,DB_INTEGER和db_integer是什麽意思,它们是打哪儿
来的?是这样,关系数据库供应商通常随API提供一些type-mapping helpers,为其所
支持的每种类型和简单的结构定义一个符号常量或者typedef,把数据库类型对应到
C/C++类型上。
下面是一段假想的数据库API头文件:
#define DB_INTEGER 1
#define DB_STRING 2
#define DB_CURRENCY 3
...
typedef long int db_integer;
typedef char db_string[255];
typedef struct {
int integral_part;
unsigned char fractionary_part;
} db_currency;
...
我们试 来写一个FetchDoubleField函数,作为走向泛型化的第一步。此函数从游标处得到
一个double值。数据库本身提供的类型映像(type mapping)是db_currency,但是我们希望
能用double的形式来操作。FetchDoubleField看上去跟FetchIntField很相似,简直就是孪
生兄弟。例2:
// Example 2: Wrapping a raw cursor double fetch operation.
//
void FetchDoubleField(db_cursor& cr, unsigned int col, double& val)
{
if (cr.column_type[col] != DB_CURRENCY)
throw std::runtime_error("Column type mismatch");
if (!db_access_column(&cr, col))
throw std::runtime_error("Cannot transfer data");
db_currency temp;
memcpy(&temp, cr.column_data[col], sizeof(temp));
db_release_column(&cr, col);
val = temp.integral_part + temp.fractionary_part / 100.;
}
看上去很像FetchIntField吧 我们可不想对每一个类型都写一个单独的函数,所以
如果能够在一个地方把FetchIntField, FetchDoubleField以及其他的Fetch函数合
为一体就好了。
我们把这两片代码的不同之处列举如下:
·输入类型:double/int
·内部类型:db_currency/db_integer
·常数值类型:DB_CURRENCY/DB_INTEGER
·算法:一个表达式/static_cast
输入类型(int/double)与其他几点之间的对应关系看上去没什麽规律可循,而是很随意,
跟数据库供应商(恰好)提供的类型关系密切。Template机制本身无能为力,它没有提供
如此先进的类型推理机制。也没法把不同的类型用继承关系组织起来,因为我们处理的是
原始类型。受到API的限制以及问题本身的底层特性,乍看上去我们好像没辙了。不过我们
还有一条活路。
进入TRAITS大门:Traits技术就是用来解决上述问题的:把与各种类型相关的代码片断合体,
并且具有类似and/or结构的能力,到时可以根据不同的类型产生不同的变体。
Traits依赖显式模版特殊化(explicit template specialization)机制来获得这种结果。
这一特性使你可以为每一个特定的类型提供模板类的一个单独实现,见例3:
// Example 3: A traits example
//
template <class T>
class SomeTemplate
{
// generic implementation (1)
...
};
// 注意下面特异的语法
template <>
class SomeTemplate<char>
{
// implementation tuned for char (2)
...
};
...
SomeTemplate<int> a; // will use (1)
SomeTemplate<char*> b; // will use (1)
SomeTemplate<char> c; // will use (2)
如果你用char类型来实例化SomeTemplate类模板,编译器会用那个显式的模板声明来特殊化。
至於其他的类型,当然就用那个通用模板来实例化。这就像一个由类型驱动if-statement。
通常最通用的模板(相当于else部份)最先定义,if-statement靠後一点。你甚至可以决定
完全不提供通用的模板,这样只有特定的实例化是允许的,其他的都会导致编译错误。
现在我们把这个语言特性跟手上的问题联系起来。我们要实现一个模板函数FetchField,
用需要读取的类型作为叁数来实例化。在该函数内部,我会用一个叫做TypeId的东西代表
那个符号常量,当要获取int型值时它的值就是DB_INTEGER,当要获取double型值时它的
值就是DB_CURRENCY。否则,就必须在编译时报错。类似的,根据要获取的类型的不同,
我们还需要操作不同的API类型(db_integer/db_currency)和不同的转换算法(表达式/static_cast).
让我们用显式模板特殊化机制来解决这个问题。我们得有一个FetchField,可以针对一个
模板类来产生不同的变体,而那个模板类又能够针对int和double进行显式特殊化。每个
特殊化都必须为这些变体提供统一的名称。
// Example 4: Defining DbTraits
//
// Most general case not implemented 最通用的情况没有实现
template <typename T> struct DbTraits;
// Specialization for int
template <>
struct DbTraits<int>
{
enum { TypeId = DB_INTEGER };
typedef db_integer DbNativeType;
// 注意下面的Convert是static member function 译者
static void Convert(DbNativeType from, int& to)
{
to = static_cast<int>(from);
}
};
// Specialization for double
template <>
struct DbTraits<double>
{
enum { TypeId = DB_CURRENCY };
typedef db_currency DbNativeType;
// 注意下面的Convert是static member function 译者
static void Convert(const DbNativeType& from, double& to)
{
to = from.integral_part + from.fractionary_part / 100.;
}
};
现在,如果你写DbTraits<int>::TypeId,你得到的就是DB_INTEGER,而对於
DbTraits<double>::TypeId,得到的就是DB_CURRENCY,对於
DbTraits<anything_else>::TypeId,得到的是什麽呢?Compile-time error!
因为模板类本身只是声明了,并没有定义。
是不是一劳永逸了?看看我们如何利用DbTraits来实现FetchField就放心了。
我们把所有变化的部份 枚举类型 API类型 转换算法 都放在了DbTraits
里,这下我们的函数里只包含FetchIntField和FetchDoubleField的相同部份了:
// Example 5: A generic, extensible FetchField using DbTraits
//
template <class T>
void FetchField(db_cursor& cr, unsigned int col, T& val)
{
// Define the traits type
typedef DbTraits<T> Traits;
if (cr.column_type[col] != Traits::TypeId)
throw std::runtime_error("Column type mismatch");
if (!db_access_column(&cr, col))
throw std::runtime_error("Cannot transfer data");
typename Traits::DbNativeType temp;
memcpy(&temp, cr.column_data[col], sizeof(temp));
Traits::Convert(temp, val);
db_release_column(&cr, col);
}
搞定了 我们只不过实现和使用了一个traits模板类而已
Traits依靠显式模板特殊化来把代码中因类型不同而发生变化的片断拖出来,用统一的
接口来包装。这个接口可以包含一个C++类所能包含的任何东西:内嵌类型,成员函数,
成员变量,作为客户的模板代码可以通过traits模板类所公开的接口来间接访问之。
这样的traits接口通常是隐式的,隐式接口不如函数签名(function signatures)那麽
严格,例如,尽管DbTraits<int>::Convert和DbTraits<double>::Convert有 非常不
同的签名,但它们都可以正常工作。
Traits模板类在各种类型上建立一个统一的接口,而又针对各种类型提供不同的实现细节。
由於Traits抓住了一个概念,一个相关联的选择集,所以能够在相似的contexts中被重用。
定义: A traits template is a template class, possibly explicitly
specialized, that provides a uniform symbolic interface over a coherent
set of design choices that vary from one type to another.

Traits模板是一个模板类,很可能是显式特殊化的模板类,它为一系列根据不同类
型做出的设计选择提供了一个统一的 符号化的接口。
TRAITS AS ADAPTERS: 用作适配子的TRAITS



No comments: