发信人: genjuro_lyb(牙神·幻色狼)
整理人: yangcs(2004-10-14 14:22:35), 站内信件
|
Generic<Programming>:类型的else-if-then机制
Andrei Alexandrescu
myan译
什么是traits?为什么人们老爱提起它,并认为它是C++泛型编程中的重要技术呢?
简单来说,traits的重要性就在于能在编译时间(compile-time)通过类型(type)确定函数的调用,尽管我们往往习惯于在运行时间(run-time)通过值(value)来确定。更妙的是,traits能让您根据其产生环境(context)作出类型判定,使得代码更清晰可读,更易于维护,这正应了那句曾解决了软件工程界无数难题的名言──“extra level of indirection(额外的中间层)”。如果正确使用traits,我们在享受上述好处的同时,亦不必付出性能、安全及耦合性等方面的代价。
例子
Traits不仅仅是纯粹的泛型编程的工具,对一些特定问题的解决亦有很大的帮助。不信?先看下面这个例子吧。
假设我们正在编写一个关系数据库应用程序。开始,我们可能会使用数据库供应商所提供的本地API库来访问数据,当然过不了多久就会发现,为了提高灵活性,使其更好地适于手中需要解决的问题,我们有必要写一些函数来包装这些原始的API。生活的色彩也因此而来,不是吗?
一般这些API能提供一些基本的功能,用以把原始数据(比如行集合或者查询结果),从游标(Cursor)处传送到内存中。OK,现在我们要在不暴露底层细节的前提下,写一个更高层次的函数,用于从一列中析取出某个值。其形式大概是下面这个样子(我们假设这些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
if (!db_access_column(&cr, col))
throw std::runtime_error("Cannot transfer data");
db_integer temp;
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);
}
这种接口函数都是我们曾经写过的:凌乱不堪、极度缺乏灵活性(highly imperative)、和大量低层细节纠缠不清……而这还仅仅是一个简单的例子。我们所期望的FetchIntField函数,应该具有更高层次的抽象性──不用考虑过多细节就能从某列的游标处析取出一个整数。
对于这种非常有用的函数,我们当然希望尽可能地重用(reuse)。那怎么做呢?泛型化重要的一步就是让它不仅仅能处理整数类型int。为此,我们得理解针对int类型的这部分代码。但是首先我们注意一下,DB_INTEGER和db_interger是什么意思,从哪儿掉下来的呢?一般来说,关系数据库的供应商们会在API中提供类型映射的辅助措施:为其支持的每种类型定义一个符号常数及一些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;
...
我们来写一个从游标处析取double值的函数FetchDoubleField,作为我们泛型化的第一步。数据库提供的类型是db_currency,不过我们需要以double的形式来操作。FetchDoubleField跟FetchIntField几乎就是一对双胞胎,非常相似。在下面的例子里,我们将以加粗的形式标记出这对“双胞胎”不同的部分。
// 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、FetchDoubleField以及其他Fetch…可以合起来只写一次,那多好啊。
那么,让我们来列举一下这对“双胞胎”的不同之处。
输入类型:double/int
内部使用类型:db_currency/db_integer
常数:DB_CURRENCY/DB_INTEGER
算法:表达式/静态转换static_cast
输入类型(double/int)跟其他几点之间没有明显的对应规则,这完全取决于数据库供应商所提供的约定(convention)和定义(definition)。模板机制本身则爱莫能助,它还不具备如此高级的类型推理能力。因为我们处理的是原始类型,继承机制亦不能把不同的类型联系起来。由于受API的限制以及问题本身的底层特性,乍一看,泛型方法已经“疑无路”了;其实,我们还可以柳暗花明。
初窥TRAITS门径
Traits正是解决此问题的灵丹妙药。它能把依赖某种类型(比如上面的double/int)、根据不同的结构及行为作出相应调整的代码段联结起来。为此,Traits依赖于C++的语言特性:显式模板特化(explicit template specialization),这种特性能让我们为每种特定的类型提供特定的模板类实现。
// 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,编译器就会采用显式特化的方案(1)。当然对于其他类型,编译器则会实例化通用模板(2)。这看起来很像if语句,只不过是由类型驱动罢了。通常最通用的模板(相当于else部分)最先定义,if语句靠后一点。我们甚至可以根本不提供通用模板,只提供特化部分,使其他的实例化都导致编译错误。
现在我们把这个语言特性与手头的问题联系起来,实现一个用读取的类型参数化的模板函数FetchField。在此函数中,我们要能作如下推断:假定一个整数常量TypeId,如果要获取的类型是int,那它的值是DB_INTEGER;如果要获取的类型是double,那么,它的值是DB_CURRENCY;否则,将会发生编译错误。类似地,我们也可以根据获取的类型的不同,来操作不同的数据类型(db_interger/db_currency)和不同的转换算法。
让我们用显式模板特化来解决这个问题,利用模板类来掩盖前面提到的那些不同之处,并针对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;
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;
static void Convert(const DbNativeType& from, double& to)
{
to = from.integral_part + from.fractionary_part / 100.;
}
};
现在写DbTraits<int>::TypeId得到DB_INTERGER,写DbTraits<double>::TypeId就得到DB_CURRENCY,写DbTraits<anything else>::TypeId呢?得到编译错误,因为模板类本身只声明而尚未定义。
有没有悟到点什么?现在看看我们怎么利用DbTraits来实现一个通用的FetchField函数,把所有的不同之处──枚举类型、数据库原生类型、转换算法──通通掩盖在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);
}
OK,搞定!我们所实现的正是一个traits类模板。
Traits依赖于显式模板特化,把代码中类型相关的不同之处封装在统一的接口中,而这种接口跟一个普通的C++类没有什么两样,可以包含嵌套类型、成员函数、成员变量,模板化的用户代码可以通过traits模板类公开的接口间接访问。
这样的traits接口通常是隐式的──这是traits类模板和使用它的代码之间的约定。隐式接口访问比函数表征更为宽松,比如尽管DbTraits<int>::Convert和DbTraits<double>::Convert这两个函数的表征不同,但由于遵从调用代码与它们之间的约定,因此都能正常运行。
Traits模板类在一组高层次上有意义的设计选择的基础上建立起统一的接口,但在实现细节上(类型、值、算法)又有所区别。由于traits记录了一个概念(concept),一组相互关联的决策,因此可能重用在类似的环境(context)中。在此例中,我们就可以在其他的数据库资料操作(比如把数据写回游标)中重用DbTraits。
定义:Traits模板是一种可以为一组相互关联而类型不同的设计选择提供统一的符号化接口(可能显式特化)的模板类。
Definition: 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 AS ADAPTERS(做适配子的Traits)
前面我们已经把数据库说得够多了,现在我们换个流行的话题──smart pointer。
假设我们在开发一个模板类SmartPtr。对于smart pointer来说,最棒的是它看起来跟普通的指针没什么两样,却能使内存管理自动化;而不太好对付的是实现它们的那些不同寻常的代码(与smart pointers相关的技术跟稀奇古怪的巫术没什么两样)。这一残酷的事实告诉我们一个重要的实践经验:我们最好尽可能一劳永逸,写出一个出色的、具有工业强度的smart pointer来满足我们绝大部分的要求。此外,常常我们不能靠修改类来配合smart pointer,于是我们的SmartPtr必须很有弹性。
很多类层次(class hierarchy)都使用引用计数(reference counting),提供相关函数管理对象生存期。然而由于引用计数的实现并没有统一的标准,各个C++库供应商的实现在语法及语义上可能都有所不同。比如,在我们的程序中,可能有下列两种接口:
我们的大部分类都实现RefCounted接口:
class RefCounted
{
public:
void IncRef() = 0;
bool DecRef() = 0; // if you DecRef() to zero
// references, the object is destroyed
// automatically and DecRef() returns true
virtual ~RefCounted() {}
};
由第三方提供的Widget类使用了一个略为不同的接口:
class Widget
{
public:
void AddReference();
int RemoveReference(); // returns the remaining
// number of references; it's the client's
// responsibility to destroy the object
...
};
我们当然不想维护两个smart pointer类。我们希望在我们自己的和基于Widget的继承层次之间共享一个统一的SmartPtr后端。一个基于traits的解决方案就能提供统一的语法及语义接口,来包装这两个略为不同的接口,也就是先建立通用模板支持RefCounted接口,然后为Widget建立特化的版本:
// Example 6: Reference counting traits
//
template <class T>
class RefCountingTraits
{
static void Refer(T* p)
{
p->IncRef(); // assume RefCounted interface
}
static void Unrefer(T* p)
{
p->DecRef(); // assume RefCounted interface
}
};
template <>
class RefCountingTraits<Widget>
{
static void Refer(Widget* p)
{
p->AddReference(); // use Widget interface
}
static void Unrefer(Widget* p)
{
// use Widget interface
if (p->RemoveReference() == 0)
delete p;
}
};
在SmartPtr里,我们这样使用RefCountingTraits:
template <class T>
class SmartPtr
{
private:
typedef RefCountingTraits<T> RCTraits;
T* pointee_;
public:
...
~SmartPtr()
{
RCTraits::Unrefer(pointee_);
}
};
您可能会提出,在上例中为什么不直接特化针对Widget的SmartPtr的构造及析构函数呢?为什么要模板特化traits而不是SmartPtr本身呢?这样还可以少掉一个多余的类呢。对此例而言,您说的都对,但是却有些我们不得不注意的缺点:
缺乏扩展性。如果SmartPtr需要再加一个参数,那就没辙了,我们不能针对Widget和一个任意类型U部分特化SmartPtr<T, U>的成员函数。顺便提一句,对于很多模板参数来说,smart pointers可是个不错的考虑(见Van Horn所举的丰富的例子[1]);【注:作者所说做不到的情形如下:
template <class T, class U>
class Demo {
public:
void dostuff() {...} //Generic版本dostuff()
};
template<> void Demo<char, int>::dostuff() {...}
//OK,针对成员函数进行特化
template <class U> void Demo<Widget, U>::dostuff() {...}
//error,由于不存在Demo<Widget, U>的部分特化定义,所以编译失败。
不过不知道是不是理解的问题,这么说未免也过于绝对。比如前面这个编译出错的地方,我们只需要在前面加入特化版本类的声明就行了,也就是加入: template <class U> Demo<Widget, U> { public: void dostuff(); } 当然这个办法很白痴,但似乎也可以达到目的。】
代码不够清晰。trait有个名字,能很好地组织相关的东西,因此使代码更易懂。相形之下,直接特化SmartPtr的成员函数难免留下斧凿的痕迹,生硬而不自然;
对同一类型不能使用多种traits。
用继承机制的解决方案,也存在上述缺陷[2],更不用说继承本身有很多值得注意的问题。解决这样的变体问题,使用继承实在太笨重了。此外,通常用以取代继承方案的另一种经典机制──containment,用在这里也显得繁琐不堪。相反,traits方案简洁明了,物合其用。
Traits扮演的一个重要角色就是接口胶合剂(interface glue)──可适应性极强的通用适配子。如果不同的类对一个给定概念的实现略微不同,traits可以把它们统一在一个公共接口下。
对一个给定类型提供多种TRAITS
现在,我们假设所有的人都很喜欢这个SmartPtr模板类,直到有一天,在多线程应用程序里开始出现神秘的bug,美梦破灭了。后来发现罪魁祸首是Widget,它的引用计数函数并不是线程安全的。现在我们不得不亲自实现Widget::AddReference和Widget::RemoveReference,最合理的位置应该是在RefCountingTraits中,打上个补丁吧:
// Example 7: Patching Widget's traits for thread safety
//
template <>
class RefCountingTraits<Widget>
{
static void Refer(Widget* p)
{
Sentry s(lock_); // serialize access
p->AddReference();
}
static void Unrefer(Widget* p)
{
Sentry s(lock_); // serialize access
if (p->RemoveReference() == 0)
delete p;
}
private:
static Lock lock_;
};
不幸的是,虽然重新编译、测试之后运行正确了,但是程序慢得像蜗牛。仔细分析之后发现,刚才的所作所为往程序里塞了一个糟糕的瓶颈。实际上只有少数几个Widget是需要能够被好几个线程访问的,余下的绝大多数Widget都是只被一个线程访问的。
我们要做的就是告诉编译器,按我们的需求分别使用多线程traits和单线程traits这两个不同版本。你的代码主要使用单线程traits。
如何告诉编译器使用那个traits?这么干:把traits作为附加模板参数传给SmartPtr。缺省情况下传递以前那个traits模板,而用特定的类型实例化特定的模板。
template <class T, class RCTraits = RefCountingTraits<T> >
class SmartPtr
{
...
};
对单线程版的RefCountingTraits<Widget>不做改动,而把多线程版放在一个单独的类中:
class MtRefCountingTraits
{
static void Refer(Widget* p)
{
Sentry s(lock_); // serialize access
p->AddReference();
}
static void Unrefer(Widget* p)
{
Sentry s(lock_); // serialize access
if (p->RemoveReference() == 0)
delete p;
}
private:
static Lock lock_;
};
现在可将SmartPtr<Widget>和SmartPtr<Widget, MtRefCountingTraits>分别用于单线程和多线程。OK了!就跟Scott Meyers可能会说的那样,“你要是没体会过快乐,就不知道怎么找乐。”
如果一种类型只要一个trait由可以应付,那么只要用显式模板特殊化就够了。现在即使一个类型需要多个trait来应付我们也搞得定。所以,traits必须能够从外界塞进来,而不是在内部“算出来”。一个应当谨记的惯用法是提供一个traits类作为最后一个模板参数。缺省的traits通过模板参数缺省值给定。
定义:一个traits类(与traits模板类相对)或者是一个traits模板类的实例,或者是一个与traits 模板类展现出相同接口的单独的类。
Definition: A traits class (as opposed to a traits template) is either an instantiation of a traits template, or a separate class that exposes the same interface as a traits template instantiation.
问题
我们对traits的讨论刚刚开始。今后的专栏中,我将论述各种情形下的traits,多用途(general-purpose)traits,以及广层次(hierarchy-wide)traits──能让我们不仅仅为某一个类,而且能一次性为整个层次或整个子层次定义traits。
但先提个问题。在我们多线程的代码中仍然有缺乏效率地方,能找出来吗?如何解决这个问题?
致谢
感谢Scott Meyers提出最初想法和专栏的名字,感谢Sorin Jianu和Herb Sutter仔细审阅了全文。
参考文献
[1]. K. S. Kevin S. Van Horn's Smart Pointers, http://www.xmission.com/~ksvhsoft/code/smart_ptrs.html.
[2]. Sutter, H. "Uses and Abuses of Inheritance, Part 2," C++ Report, 11(1): 58–61, Jan. 1999.
----
|
|