荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: Dreamer (黄昏·落霞·萤火·街灯), 信区: SoftDev
标  题: 【C++】[FAQ]为什么我无法限制模板的参数?
发信站: 荔园晨风BBS站 (Wed Jun 16 14:18:49 2004), 站内信件


Q: 为什么我无法限制模板的参数?
A: 呃,其实你是可以的。而且这种做法并不难,也不需要什么超出常规的技巧。

让我们来看这段代码:

        template<class Container>
        void draw_all(Container& c)
        {
                for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
        }
如果c不符合constraints,出现了类型错误,那么错误将发生在相当复杂的
for_each解析之中。比如说,参数化的类型被要求实例化int型,那么我们无法为
之调用Shape::draw()。而我们从编译器中得到的错误信息是含糊而令人迷惑的—
—因为它和标准库中复杂的for_each纠缠不清。
为了早点捕捉到这个错误,我们可以这样写代码:

        template<class Container>
        void draw_all(Container& c)
        {
                Shape* p = c.front(); // accept only containers of Shape*s

                for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
        }
我们注意到,前面加了一行Shape *p的定义(尽管就程序本身而言,p是无用的)
。如果不可将c.front()赋给Shape *p,那么就大多数现代编译器而言,我们都可
以得到一条含义清晰的出错信息。这样的技巧在所有语言中都很常见,而且对于所
有“不同寻常的构造”都不得不如此。[译注:意指对于任何语言,当我们开始探
及极限,那么不得不写一些高度技巧性的代码。]

不过这样做不是最好。如果要我来写实际代码,我也许会这样写:
        template<class Container>
        void draw_all(Container& c)
        {
                typedef typename Container::value_type T;
                Can_copy<T,Shape*>(); // accept containers of only Shape*s

                for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
        }


这就使代码通用且明显地体现出我的意图——我在使用断言[译注:即明确断言
typename Container是draw_all()所接受的容器类型,而不是令人迷惑地定义了一
个Shape *指针,也不知道会不会在后面哪里用到]。Can_copy()模板可被这样定义

        template<class T1, class T2> struct Can_copy {
                static void constraints(T1 a, T2 b) { T2 c = a; b = a; }
                Can_copy() { void(*p)(T1,T2) = constraints; }
        };
Can_copy在编译期间检查确认T1可被赋于T2。Can_copy<T,Shape*>检查确认T是一
个Shape*类型,或者是一个指向Shape的公有继承类的指针,或者是用户自定义的
可被转型为Shape *的类型。注意,这里Can_copy()的实现已经基本上是最优化的
了:一行代码用来指明需要检查的constraints[译注:指第1行代码;
constraints为T2],和要对其做这个检查的类型[译注:要作检查的类型为T1] ;
一行代码用来精确列出所要检查是否满足的constraints(constraints()函数)
[译注:第2行之所以要有2个子句并不是重复,而是有原因的。如果T1,T2均是用
户自定义的类,那么T2 c = a; 检测能否缺省构造;b = a; 检测能否拷贝构造]
;一行代码用来提供执行这些检查的机会 [译注:指第3行。Can_copy是一个模板
类;constraints是其成员函数,第2行只是定义,而未执行] 。

[译注:这里constraints实现的关键是依赖C++强大的类型系统,特别是类的多态
机制。第2行代码中T2 c = a; b = a; 能够正常通过编译的条件是:T1实现了T2的
接口。具体而言,可能是以下4种情况:(1) T1,T2 同类型 (2) 重载operator
= (3) 提供了 cast operator (类型转换运算符)(4) 派生类对象赋给基类指针
。说到这里,记起我曾在以前的一篇文章中说到,C++的genericity实作——
template不支持constrained genericity,而Eiffel则从语法级别支持
constrained genericity(即提供类似于template <typename T as
Comparable> xxx 这样的语法——其中Comparable即为一个constraint)。曾有读
者指出我这样说是错误的,认为C++ template也支持constrained genericity。现
在这部分译文给出了通过使用一些技巧,将OOP和GP的方法结合,从而在C++中巧妙
实现constrained genericity的方法。对于爱好C++的读者,这种技巧是值得细细
品味的。不过也不要因为太执著于各种细枝末节的代码技巧而丧失了全局眼光。有
时语言支持方面的欠缺可以在设计层面(而非代码层面)更优雅地弥补。另外,这
能不能算“C++的template支持constrained genericity”,我保留意见。正如,
用C通过一些技巧也可以OOP,但我们不说C语言支持OOP。]

请大家再注意,现在我们的定义具备了这些我们需要的特性:
你可以不通过定义/拷贝变量就表达出constraints[译注:实则定义/拷贝变量的工
作被封装在Can_copy模板中了] ,从而可以不必作任何“那个类型是这样被初始化
”之类假设,也不用去管对象能否被拷贝、销毁(除非这正是constraints所在)
。[译注:即——除非constraints正是“可拷贝”、“可销毁”。如果用易理解的
伪码描述,就是template <typename T as Copy_Enabled> xxx,template
<typename T as Destructible> xxx 。]
如果使用现代编译器,constraints不会带来任何额外代码
定义或者使用constraints均不需使用宏定义
如果constraints没有被满足,编译器给出的错误消息是容易理解的。事实上,给
出的错误消息包括了单词“constraints” (这样,编码者就能从中得到提示)、
constraints的名称、具体的出错原因(比如“cannot initialize Shape* by
double*”)
既然如此,我们干吗不干脆在C++语言本身中定义类似Can_copy()或者更优雅简洁
的语法呢?The Design and Evolution of C++分析了此做法带来的困难。已经有
许许多多设计理念浮出水面,只为了让含constraints的模板类易于撰写,同时还
要让编译器在constraints不被满足时给出容易理解的出错消息。比方说,我在
Can_copy中“使用函数指针”的设计就来自于Alex Stepanov和Jeremy Siek。我认
为我的Can_copy()实作还不到可以标准化的程度——它需要更多实践的检验。另外
,C++使用者会遭遇许多不同类型的constraints,目前看来还没有哪种形式的带
constraints的模板获得压倒多数的支持。

已有不少关于constraints的“内置语言支持”方案被提议和实作。但其实要表述
constraint根本不需要什么异乎寻常的东西:毕竟,当我们写一个模板时,我们拥
有C++带给我们的强有力的表达能力。让代码来为我的话作证吧:

        template<class T, class B> struct Derived_from {
                static void constraints(T* p) { B* pb = p; }
                Derived_from() { void(*p)(T*) = constraints; }
        };

        template<class T1, class T2> struct Can_copy {
                static void constraints(T1 a, T2 b) { T2 c = a; b = a; }
                Can_copy() { void(*p)(T1,T2) = constraints; }
        };

        template<class T1, class T2 = T1> struct Can_compare {
                static void constraints(T1 a, T2 b) { a==b; a!=b; a<b; }
                Can_compare() { void(*p)(T1,T2) = constraints; }
        };

        template<class T1, class T2, class T3 = T1> struct Can_multiply {
                static void constraints(T1 a, T2 b, T3 c) { c = a*b; }
                Can_multiply() { void(*p)(T1,T2,T3) = constraints; }
        };

        struct B { };
        struct D : B { };
        struct DD : D { };
        struct X { };

        int main()
        {
                Derived_from<D,B>();
                Derived_from<DD,B>();
                Derived_from<X,B>();
                Derived_from<int,B>();
                Derived_from<X,int>();

                Can_compare<int,float>();
                Can_compare<X,B>();
                Can_multiply<int,float>();
                Can_multiply<int,float,double>();
                Can_multiply<B,X>();

                Can_copy<D*,B*>();
                Can_copy<D,B*>();
                Can_copy<int,B*>();
        }

        // the classical "elements must derived from Mybase*" constraint:

        template<class T> class Container : Derived_from<T,Mybase> {
                // ...
        };
事实上Derived_from并不检查继承性,而是检查可转换性。不过Derive_from常常
是一个更好的名字——有时给constraints起个好名字也是件需细细考量的活儿。




--
 if (I.amOnBBS) I.OpenMyFTP();
 else { if (BBS.isShutDown) {if (rand()<RAND_MAX*3/4) I.OpenMyFTP();}
        else { if (MyMachine.hasSthWrong) I.SayHehe();
               else I.GoOutDoor();
               You.ExcuseMeFor(I.canDoNothing); }
 }

※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 192.168.0.223]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店