|
楼主 |
发表于 2004-5-7 18:30:28
|
显示全部楼层
<FONT size=+0><!-- 固定文字颜色 -->6.多重继承(由MosesJones提问)
有三个相互关联的问题:
您认为多重继承是一门真正的OO语言所必需的吗?
在使用多重继承设计系统时,您认为需要避免哪些陷阱(尤其在可维护性方面)?
您认为有哪些措施,可以简化多重继承的可读性,以减少新手可能做的危险动作?
Bjarne:假如你依赖于静态(编译期)类型检查,你将需要MI。如果没有MI的话,许多程序将被“扭曲”,并且你将不得不过于频繁地使用显式转型操作(explicit type conversion (casting))。我不会声称自己知道什么是“一门真正的OO语言” (即使果真有这么一门语言),但是,假如你本质上必须始终使用显式类型查询(explicit type inquiry)的话,你使用的就不是一门真正的OO语言。
我并不认为MI是一个特别严重的陷阱来源。MI与单继承以及任何其他强大的语言特性所共有的明显的问题,就是被滥用。我倾向于在简单的聚合(aggregation)中以及实现多个接口时使用MI,例如:
class network_file_error : public network_error, public file_error {
// ...
};
还有
class interface { // abstract class
// pure virtual functions
};
class base_implementation { // useful functionality
// data, functions, virtual functions
};
class my_implementation : public interface, protected base_implementation {
// data, functions
// override some base_implementation functions
// override all interface functions
};
在后一个例子中,你可以让用户仅仅通过指向接口的指针或引用来访问my_implementation。这使得用户代码可以不依赖于“实现类(implementation class)”,而且系统不会遭遇所谓的“脆弱的基类”这个问题。“实现类”可被更换为一个改进版,而无需重新编译用户代码(除了my_implementation自身)。
遵循这两种风格可以避免绝大多数问题。当然,你可以在《The C++ Programming Language》第三版和特别版中找到更加广泛的讨论。(如需了解详细情况、样章或评论等,请访问我的个人主页。)
实际上,关于MI的问题通常意味着提问者已迷失于技术细节之中。对于几乎所有程序来说,使用抽象类、模板、异常以及标准库的所带来益处远远超过其招致的问题。在一门带有继承的静态类型的语言中,MI必不可少,但它并不属于那些应该频繁使用的特性。
定义具体类(concrete classes)以表示简单的类型,定义抽象类(abstract classes)以表示主要的接口,如果你将重点集中于此,可能会产生一个良好的设计。所以说,完成某个程序可能需要也可能不需要用到MI,但当真正需要的时候,它显然是个优美的解决方案。
7.问题(由Edward Kmett提问)
“constrained templates”有希望被引入C++吗?对程序员来说,当前使用模板是件磨炼毅力的事。我听说在模板引入之初,“constrained genericity”就在标准委员会出现了,有没有什么重新思考那个决定的想法呢?
另一个问题在于“从Eiffel社群获得了大量动力”的“Design by Contract”,我很希望看到它逐步被纳入C++标准,但我怀疑能不能心想事成。
最后,Bjarne先生,您说过“当可以在C++中使用引用计数(reference counting)时(而不是“假如可以......”),它将是一种可选的技术”,有一本关于面向对象编程语言的书曾引用了这句话(但此刻我在Amazon上贴出ISBN也没能找到这本书)。在引用计数对象技术的前沿有没有取得重大进展呢?自从您的话被引用后,您的观点是否发生了什么变化?
Bjarne:事实上,我当时说的大意是“当自动垃圾回收机制(auotmatic garbage collection)成为C++的一部分时(而不是“假如自动垃圾回收机制......”),它将是一种可选的技术”。
对于使用频率不太高的资源来说,“引用计数”可能会非常有用,但我并不提倡将它作为一项“跟踪内存”的通用机制。C++拥有良好的内存管理机制(比如构造器、析构器以及标准库容器),但如果你需要更加“自动”的东西,插入(plugging in)一个可用的垃圾回收器(garbage collectors)是个恰当的选择。(可参见《The C++ Programming Language》C.9.1节、我的C++网页或者我的FAQ。)
顺便说几句,精装特别版《TC++PL》的ISBN是0-201-700735,而平装第三版的ISBN是0-201-889544。
对模板进行约束且不使其能力受损,这并不像听起来那么简单。详细讨论请参阅D&E。问题之一在于:如果你根据基类来表达模板参数约束的话,将会使你的系统设计向“每个属性(property)都作为一个基类”的畸形风格发展。这极易导致滥用多重继承和间接表达“本来该直接表达”的东西。例如,说“一个类必须拥有一个<<操作符”比“必须继承自‘Printable’(才能拥有一个<<操作符)”更加清晰。表达更直接的代码,也就更加简短,而且生成的代码比“添加一个基类来表达一种约束”的版本品质更佳。
特化(Specialization)和部分特化(partial specialization)机制为人们提供了许多希望从constraints中获得的表达能力。例如,假如我有一个通用的排序函数模板
template< class Container> void mysort(Container& c);
我还想为vector提供特别的排序算法,那这么写就可以了:
template< class T> void mysort(vector< T>& v);
我更欢迎带有类型错误机制的模板(templates with type errors)所提供的错误信息。其中一些可以通过更好的编译器技术而实现(人们正在为此努力),有些信息则需要某种模板参数约束或检查才能做到,但我不知道如何做到这一点。幸运的是,程序员可以帮忙。考虑我在回答问题1时给出的例子:
template< class Container>
void draw_all(Container& c)
{
for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
}
如果有一个类型错误,将会在那个相当复杂的for_each()调用决议(resolution)时发生。例如,如果容器内的元素类型为int,那么我们将会得到某种与调用for_each()相关的含糊的错误信息(因为我们不能对int调用Shape::draw())。
我编写的代码其实如下:
template< class Container>
void draw_all(Container& c)
{
Shape* p = c.front(); // accept only Shape*s
for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
}
当无用的变量“p”在初始化时,目前绝大多数编译器都将会引发一个容易理解的错误信息。类似这样的技巧在所有的语言中都很常见,并且已经开发用于所有新颖的构造(constructs)。在“产品代码”中,我可能会这样写:
template< class Container>
void draw_all(Container& c)
{
typedef typename Container::value_type T;
assert_equiv< T,Shape*>(); // accept only Shape*s
for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
}
我使用了一个断言以使它更加清晰。我把assert_equiv()的定义作为一个练习留给读者J
这引入了对问题的第二部分 — “design by Contract”的使用。既然“design by Contract”是一种设计风格,那么就你可以在C++中使用它。为此,你需要系统地使用断言。我个人倾向于依靠一些“特化的断言模板(specialized assert templates)”(请参阅《TC++PL》第三版)。我也认为这样的一些模板是“作为库的一部分而标准化”的优秀候选者。
然而,我并不认为语言将会直接支持类似于preconditions和postconditions这样的东西,我也并不认为使用与程序自身本质上相同的语言而编写的preconditions和postconditions是对asserts()的显著改进。还有,我怀疑对类层次结构进行检查(语言支持的conditions已提供)是否值得。通过在目前标准C++中更加系统地使用断言,人们今天可以获得非常显著的好处。
</FONT> |
|