网页功能: 加入收藏 设为首页 网站搜索  
Generic: 再谈Min和Max
发表日期:2006-08-30作者:[转贴] 出处:  

Andrei Alexandrescu
ye_feng译 本文代码
在1995年1月,Scott Meyers在他发表于C++ Report里的一篇名为《min, max and more》[1]的文章里,对C++社群提出 了一个挑战。在仔细地分析了基于宏和代表当时模板技术水平的实现后,他得出结论: 那么究竟什么才是最好的方法——正确 的方法——来实现max?借用Tevye的不朽名言来说:“我告诉你:我不知道。”面对以上的分析,我越来越觉得我是在告诉人 们使用宏的方法可能是最好的,但我讨厌宏。如果你知道max的一种优雅实现,请告诉我。 据我所知,这个问题跟六年前一样 富有挑战性。本文将尝试这个挑战。
Min和Max
好,我们先来回顾一下Scott的问题。基于宏的min一般看起来是这样的:
#define min(a, b) ((a) < (b) ? (a) : (b))

这个宏工作得很好,因为它非常通用(能支持所有表达式,只要operator<和operator?:有意义)。不幸的是min总是要将每 个参数计算两次,这可能会引起很多混乱。它的用法看上去太象一个函数了,但它的行为却不象。(关于min和一般的宏所引起 的各种问题,如果你想了解更广泛的讨论,请参考Herb的文章[3]。) 在C++标准库里,提供了一个基于模板的解决方案,简单 有效:
template <class T>
const T& min(const T& lhs, const T& rhs)
{
    return lhs < rhs ? lhs : rhs;
}

你可以看到,这个方法在所有地方(参数和返回值)都用了const,这是它的问题之一。假设你想做这样一件事:把两个浮点数a 和b中的较小的一个的值加上2。那么你会想这么写:
double a, b;
...
min(a, b) += 2;

如果用基于宏的min,这样一点也没有问题,但是基于模板的那个就不行了,因为你不能修改const对象。Scott指出,引入第二 个版本:
template <class T>
T& min(T& lhs, T& rhs)
{
    return lhs < rhs ? lhs : rhs;
}

同样不能令人满意,因为编译器无法对付混合情况——两个参数一个是const而另一个不是。 而且,模板不能很好地处理自动 类型转换和提升(promotion),就是说下面的代码不能编译:
int a;
short int b;
...
int smallest = min(a, b); // error: can't figure out T
// in template instantiation

不用说,基于宏的min在需要类型转换的情况下还是可以很好工作。而用基于模板的方法,你就必须写:
int smallest = min(a, int(b)); // aha, T is int

提供一个好的min/max实现的要点就是:它的行为要类似于宏,而又没有宏的各种问题。
一个(几乎是)好的开始
下面这个聪明的方法是打破成规思维方式的一个好例子:
template <class L, class R>
class MinResult {
    L& lhs_;
    R& rhs_;
public:
    operator L&() { return lhs_ < rhs_ ? lhs_ : rhs_; }
    operator R&() { return lhs_ < rhs_ ? lhs_ : rhs_; }
    MinResult(L& lhs, R& rhs) : lhs_(lhs), rhs_(rhs) {}
};
template <class LR>
class MinResult<LR, LR> {
    LR& lhs_;
    LR& rhs_;
public:
    operator LR() { return lhs_ < rhs_ ? lhs_ : rhs_; }
    MinResult(LR& lhs, LR& rhs) : lhs_(lhs), rhs_(rhs) {}
};
template <class L, class R>
MinResult min(L lhs, R rhs)
{
    return MinResult(lhs, rhs);
}

我们需要用部分特化的MinResult<LR, LR>来把两个运算符合成一个,否则operator L&和operator R&就会形成重复定 义。
基于MinResult的方法把计算推迟到必须的时候,只有在取得结果的时候才会去做计算。比如:
int a, b;
...
min(a, b);

实际上什么也不做。另一方面,如果你写:
int c = min(a, b);

编译器会调用min返回的MinResult<int, int>临时对象的operator int&,这个函数会进行计算并且返回正确的结果。
尽管你能够很好地修正const引起的问题(上面没有提到),基于MinResult的方法还是不能令人满意,因为有二义性的问题。 考虑下面的代码:
int a;
short int b;
extern Fun(int);
extern Fun(short int);
...
Fun(min(a, b)); // error! Don't know which overload to invoke!

MaxResult<int, short int>支持两种转换:转成int&和转成short int&。结果编译器无法在Fun的两个重载版本中决定选择 哪一个。而基于宏的方法再一次通过了测试,如你所期望的那样,最终会调用Fun(int)。
寻找一个类型
真正能够解决问题的应该是这样一个工具:对于两个类型L和R,能够得到min(L,R)的恰当类型。例如,如果L是char,R是int,那 么结果应该是int。假设我们已经有了这么一个工具(让我们叫它MINTYPE),我们就可以得到最终的解决方案,即下面四个函数:
template <class L, class R>
MINTYPE(L, R)
Min(L& lhs, R& rhs)
{ return lhs < rhs ? lhs : rhs; }
template <class L, class R>
MINTYPE(const L, R)
Min(const L& lhs, R& rhs)
{ return lhs < rhs ? lhs : rhs; }
template <class L, class R>
MINTYPE(L, const R)
Min(L& lhs, const R& rhs)
{ return lhs < rhs ? lhs : rhs; }
template <class L, class R>
MINTYPE(const L, const R)
Min(const L& lhs, const R& rhs)
{ return lhs < rhs ? lhs : rhs; }

这四个重载的Min对应于const和非const参数的四种可能的组合。
到现在为止,一切顺利。但是怎么定义MINTYPE呢?好,能进行类型计算的神圣的技术之一就是traits[4]。当然,我们可以这 样来得到Min的类型:
#define MINTYPE(L, R) typename MinTraits<L, R>::Result
template <class L, class R> struct MinTraits;
// Specialization for the L == R case
template <class LR> struct MinTraits<LR, LR> {
    typedef LR& Result;
};
// Specialization for bool and char
template <> struct MinTraits<bool, char> {
    typedef char Result;
};
...

这虽然可行,但是你要写非常非常多的代码。C++里一共有14种算术类型,你必须对于它们间的每一种组合都写一个特化的 MinTraits。然后你还必须加入const变种。有一些技巧可以帮你简化这件工作,比如说宏,但是这还不是一流的解决方案。
即使那样,这个方案还不完整。你必须考虑指针和用户定义的类。还有,如果对于基类和派生类调用Min怎么办?比如说你定义 了一个Shape类,而且定义了operator<来根据面积对Shape对象排序。
class Shape {
    ...
    unsigned int Area() = 0;
};
bool operator<(const Shape& lhs, const Shape& rhs) {
    return lhs.Area() < rhs.Area();
}
class Rectangle : public Shape { ... };
void Hatch(Shape& shape)
{
    Rectangle frame;
    ...
    Shape& smallest = Min(shape, frame);
    ... use smallest ...
}

如果上面调用的那个Min可以推算出Rectangle派生自Shape,并且返回Shape的一个引用,那不是很好吗?因为Rectangle的 引用可以自动转换为Shape的引用,所以这是很合理的。
但是,在你开始想做这件事的时候,你的想法已经超出min宏所能做的了。对于上面的例子,min宏不能正确工作,因为表达 式:
shape < frame ? shape : frame

将把两部分都转换成同样的类型,所以它相当于:
shape < frame ? shape : Shape(frame)

这不是我们想要的。事实上,它做了一件很糟糕的事——对象切片(slicing)。
这篇文章将讲述一个Min的实现,它能让你得到基于宏的版本的所有优点,并且更多。更好的是,这个实现大小也很合理——总 共只有80行左右代码(还包括Max)。感兴趣吗?把你的咖啡放到微波炉里再热一下,我们慢慢谈。
Loki
Okey,刚才我撒谎了。代码只有80行,但这没有把使用的类库计算在内。更确切地说,我用了Loki,在我写的书[5]里介绍的一个 通用库。在众多功能中,Loki提供了高级的类型操纵手段。Min实现里用到的Loki工具有:
1. Typelists。除了保存的是类型而不是值外,Typelists[6]和通常的列表一样。例如下面语句:
typedef TYPELIST_3(float, double, long double) FloatingPointTypes;

创建了一个包含三个类型的typelist,保存在FloatingPointTypes里。给出一个typelist(例如FloatingPointTypes)和任意一 个类型T,你可以通过一个编译时的算法Loki::TL::IndexOf得到T在typelist里的位置,例如:
Loki::TL::IndexOf<FloatingPointTypes, double>::value

结果等于1。如果类型不在typelist里,结果是-1。
2. 我们要用到的第二个工具是Select类模板,它在[7]里有详细描述。简单来说,Select允许你根据一个编译时的布尔常量在 两个类型中选择一个。例如:
typedef Loki::Select<sizeof(wchar_t) <
sizeof(short int), wchar_t, short int>::Result
SmallInt;

把SmallInt定义为wchar_t和short int中最小的整数类型。
3. TypeTraits该类模板可以进行各种关于类型的推演,例如“这个类型是指针吗?它指向什么类型?”等等。我们只用到 TypeTraits中的NonConstType类型定义。TypeTraits<T>::NonConstType是一个typedef,用来去掉T的const修饰,如 果有的话。
4. 最后,我们会用到Conversion类[7],它可以检测任意一个类型是否可以被隐式地转换为另一个类型。Conversion是实现 上面提到的关于Shape和Rectangle的魔术的基础。
MinMaxTraits类模板
为了简化类型计算,我建立了一个简单的线性层次,列出各种算术类型。基本上我是按照特定顺序列出所有算术类型的:对于任 意两个算术类型,Min的结果都是排在列表后面的那个。下面就是这个列表(现在先不考虑const):
namespace Private
{
    typedef TYPELIST_14(
        const bool,
        const char,
        const signed char,
        const unsigned char,
        const wchar_t,
        const short int,
        const unsigned short int,
        const int,
        const unsigned int,
        const long int,
        const unsigned long int,
        const float,
        const double,
        const long double)
    ArithTypes;
}

基本上,无符号类型在有符号类型的后面,尺寸大的类型在尺寸小的后面,浮点类型在整数类型后面。举例来说,如果你把一个 long int和一个double传给Min,那么结果将是double类型,因为在ArithTypes中double排在long int后面。
现在来看求Min结果类型的算法。给出两个非引用的类型L和R,那么步骤是这样的:
先假设Result为R。
如果R可以被隐式地转换为L,那么把Result改成L。
如果L和R是算术类型,并且在上面的Private::ArithTypes里R排在L后面,那么把Result改为R。这一步用来处理所有算术转 换。
如果L&可以被自动转换为R&,而不需要引入临时变量,那么把Result改为R&。这一步确保Min(frame, shape) 之类的调用返 回Shape&。
如果R&可以被自动转换为L&,而不需要引入临时变量,那么把Result改为L&。这一步确保Min(shape,frame) 之类的调用返回 Shape&。
你可以在下载的代码里看到MinMaxTraits的实现。上面算法里最难的部分就是判断“不牵涉临时变量的转换”。本质上,当一 个非const的T的引用可以转换非const的U的引用时,T就可以转换U而不引入临时变量。
Min和Max重载函数
Min和Max各有四个重载函数,对应于const和非const参数的四种组合。为了防止上面Shape/Rectangle例子里所讨论的对 象切片(slicing)问题,Min的实现和经典的a < b ? a : b略有不同:
template <class L, class R>
typename MinMaxTraits<L, R>::Result
Min(L& lhs, R& rhs)
{ if (lhs < rhs) return lhs; return rhs; }
template <class L, class R>
typename MinMaxTraits<const L, R>::Result
Min(const L& lhs, R& rhs)
{ if (lhs < rhs) return lhs; return rhs; }
... two more overloads ...
.. similar Max implementation ...

两个return语句保证了正确的类型转换,而不会产生对象切片。四个重载函数覆盖了所有混合情况,例如:Min(a + b, c + d) 或Min(a + b, 5)。
分析
让我们来看看这个新开发的Min是怎么满足Scott Meyers的要求的。他认为一个好的Min/Max实现应该能做到下面四件事:
1. 提供函数调用的语义(包括类型检查),而不是宏的语义。Min显然可以做到。
2. 支持const和非const参数(包括在同一个调用里混用)。由于那四个重载函数,Min支持const和非const参数的任意组合。
3. 支持两个不同类型的参数(当然指有意义的情况)。Min的确支持不同类型的参数,而且还有很多宏和简单模板方法所达不到 的智能:Min会分辨各种算术类型,并进行合理的转换。类型转换的选择过程(基于Private::ArithTypes)可以由库的作者控 制。
4. 不需要显式实例化。Min不需要显式实例化。
Min对于指针也可以正确工作(甚至指向不同但有关联的类型的指针,如Shape*和Rectangle*)。这是由于算法中的第一步。
一个值得注意的功能是:Min用了一个可以由你配置的算法来推导结果类型,而不限于类型系统里预先定义的规则。如果你对这 里的算法不满意,你通过对它进行调整,可以做到相当多你想做的事情,包括根据语义进行类型选择。例如,unsigned char 和unsigned int的最小值始终选unsigned char类型,因为unsigned char的值域包含在unsigned int里。通过改变类型推 导算法,你可以做到这样 “聪明” 的类型选择。
到目前为止,一切都非常好,但是还有一点细节需要说明。很遗憾,我可以得到的所有编译器都不能编译Min。公正地说,每个 编译器都在不同的代码段中翻了船。我确信代码是正确的,因为如果把那些编译器组合起来就可以编译了。但到现在为止,我还 没有见到过一个可以运行的例子。所以,如果你可以得到一个最新的编译器,并且能够试试这段代码的话,请告诉我。
Look Ahead in Anger
这些天我在读Ray Kurzweil的《The Age of Spiritual Machines》[8]。Kurzweil认为——而且相当有说服力——在2020年 代你将可以用1000美元买到有和人脑一样能力的机器。
当我想到人们——也许就是我自己,希望只是有一点点老,但聪明得多——在20年后再读这篇文章时的情形,我就忍不住想 笑。“哎呀,在2001年,这帮家伙用当时最流行的编程语言,连实现min和max这种不需要大脑的东西也会有麻烦。哈,这个 人还写了一整篇文章,用了这么多深奥的技术,才把min和max搞定。”
是不是min和max不重要呢?我不这么认为。最小和最大是出现在数学和现实生活中的两个简单概念。如果一个编程语言无法表 达数学和生活中的简单概念,那么这个语言一定有很严重的问题。“妈妈,我不在乎单词和文法,我只想写诗!”如果你必须加 入一些临时变量,然后写“a < b ? a : b”,而这时候你脑子里实际上想的是“min(expr1, expr2)”,这说明你碰到一个严 重问题:你是在用一个只会计算任意两个表达式的最小值的机器工作,而它不能表达最小值的概念。
这里有些不对劲,不是吗?C++不是唯一要指责的。Java和C#——两个被认为更高级的新语言——同样完全没有能力表达min 和max。因为,你知道,min和max不是对象。
也许将来这个时期会被称为“对象疯狂”。谁知道呢……我不禁懊恼地问:“程序员,你究竟去向何方?”
致谢
感谢所有在Usenet上参加关于volatile讨论的人们,特别是Dave Butenhof,Kenneth Chiu,James Kanze,Kaz Kylheku,Tom Payne和David Schwartz。
参考书目
[4] A. Alexandrescu. "Traits: the else-if-then of Types," C++ Report, April 2000.
[5] A. Alexandrescu. Modern C++ Design (Addison-Wesley Longman, 2001).
[6] J. Vlissides and A. Alexandrescu. "To Code or Not to Code," C++ Report, March 2000.
[7] A. Alexandrescu. "Generic: Mappings between Types and Values," C/C++ Users Journal Experts Forum, September 2000, http://www.cuj.com/experts/1810/alexandr.htm.
[8] R. Kurzweil. The Age of Spiritual Machines: When Computers Exceed Human Intelligence (Penguin USA, 2000).
 
我来说两句】 【加入收藏】 【返加顶部】 【打印本页】 【关闭窗口
中搜索 Generic: 再谈Min和Max
本类热点文章
  C++ 常用模板武道会
  深入运算符new 返回值
  实现一个脚本引擎
  C/C++ 应用漫谈一
  剖析C++模板(下)
  C++源代码游戏编程--WinMain()函数集
  怎样在游戏中实现脚本控制
  角色扮演游戏引擎的设计原理
  RPG脚本之道
  C++ STL简介
  STL 简介,标准模板库
  Generic: 再谈Min和Max
最新分类信息我要发布 
最新招聘信息

关于我们 / 合作推广 / 给我留言 / 版权举报 / 意见建议 / 广告投放  
Copyright ©2003-2024 Lihuasoft.net webmaster(at)lihuasoft.net
网站编程QQ群   京ICP备05001064号 页面生成时间:0.00346