《Effective C++》条款26 防卫潜伏的ambiguity模棱两可的状态

每个人都有思想。有些人相信自由经济学,有些人相信来生。有些人甚至相信COBOL是一种真正的程序设计语言。C++也有一种思想:它认为潜在的二义性不是一种错误。ambiguity

这是潜在二义性的一个例子:

class B; // 对类B提前声明
//
class A {
public:
A(const B&);
// 可以从B构造而来的类A
};

class B {
public:
operator A() const; // 可以从A转换而来的类B
};

这些类的声明没一点错——他们可以在相同的程序中共存而没一点问题。但是,看看下面,当把这两个类结合起来使用,在一个输入参数为A的函数里实际传进了一个B的对象,这时将会发生什么呢?

void f(const A&);

B b;

f(b); // 错误!——二义

一看到对f的调用,编译器就知道它必须产生一个类型A的对象,即使它手上拿着的是一个类型B的对象。有两种都很好的方法来实现(见条款M5)。一种方法是调用类A的构造函数,它以b为参数构造一个新的A的对象。另一种方法是调用类B里自定义的转换运算符,它将b转换成一个A的对象。因为这两个途径都一样可行,编译器拒绝从他们中选择一个。

当然,在没碰上二义的情况下,程序可以使用。这正是潜在的二义所具有的潜伏的危害性。它可以长时期地潜伏在程序里,不被发觉也不活动;一旦某一天某位不知情的程序员真的做了什么具有二义性的操作,混乱就会爆发。这导致有这样一种令人担心的可能:你发布了一个函数库,它可以在二义的情况下被调用,而你却不知道自己正在这么做。

另一种类似的二义的形式源于C++语言的标准转换——甚至没有涉及到类:

void f(int);
void f(char);

double d = 6.02;

f(d); // 错误!——二义

d是该转换成int还是char呢?两种转换都可行,所以编译器干脆不去做结论。幸运的是,可以通过显式类型转换来解决这个问题:

f(static_cast<int>(d)); // 正确,
调用f(int)
f(static_cast<char>(d)); // 正确, 调用f(char)

多继承(见条款43)充满了潜在二义性的可能。最常发生的一种情况是当一个派生类从多个基类继承了相同的成员名时:

class Base1 {
public:
int doIt();
};
class Base2
{
public:
void doIt();
};

class Derived: public Base1, // Derived没有声明
public Base2 { //
一个叫做doIt的函数
...

};

Derived d;

d.doIt(); // 错误!——二义

当类Derived继承两个具有相同名字的函数时,C++没有认为它有错,此时二义只是潜在的。然而,对doIt的调用迫使编译器面对这个现实,除非显式地通过指明函数所需要的基类来消除二义,函数调用就会出错:

d.Base1::doIt(); // 正确,
调用Base1::doIt

d.Base2::doIt(); // 正确, 调用Base2::doIt

这不会令很多人感到麻烦,但当看到上面的代码没有用到访问权限时,一些本来很安分的人会动起心眼想做些不安分的事:

class Base1 { ... }; // 同上

class Base2 {
private:
void doIt(); //
此函数现在为private
};

class Derived: public Base1, public Base2
{ ... }; //
同上

Derived d;

int i = d.doIt(); // 错误! — 还是二义!

对doIt的调用还是具有二义性,即使只有Base1中的函数可以被访问。另外,只有Base1::doIt返回的值可以用于初始化一个int这一事实也与之无关——调用还是具有二义性。如果想成功地调用,就必须指明想要的是哪个类的doIt。

C++中有一些最初看起来会觉得很不直观的规定,现在就是这种情况。具体来说,为什么消除“对类成员的引用所产生的二义”时不考虑访问权限呢?有一个非常好的理由,它可以归结为:改变一个类成员的访问权限不应该改变程序的含义。

比如前面那个例子,假设它考虑了访问权限。于是表达式d.doIt()决定调用Base1::doIt,因为Base2的版本不能访问。现在假设Base1的Doit版本由public改为protected,Base2的版本则由private改为public。

转瞬之间,同样的表达式d.doIt()将导致另一个完全不同的函数调用,即使调用代码和被调用函数本身都没有被修改!这很不直观,编译器甚至无法产生一个警告。可见,不是象你当初所想的那样,对多继承的成员的引用要显式地消除二义性是有道理的。

既然写程序和函数库时有这么多不同的情况会产生潜在的二义性,那么,一个好的软件开发者该怎么做呢?最根本的是,一定要时时小心它。想找出所有潜在的二义性的根源几乎是不可能的,特别是当程序员将不同的独立开发的库结合起来使用时(见条款28),但在了解了导致经常产生潜在二义性的那些情况后,你就可以在软件设计和开发中将它出现的可能性降到最低。

时间: 2024-08-27 06:00:12

《Effective C++》条款26 防卫潜伏的ambiguity模棱两可的状态的相关文章

Effective C++ 条款26

尽可能延后变量定义式的出现时间 我们知道定义一个对象的时候有一个不争的事实,那就是分配内存.如果是我们自定义的对象,程序执行过程中会调用类的构造函数和析构函数. 我们打个比方,如果天下雨了,你带把雨伞肯定是值得的.但是,如果你带伞了,今天却没下雨,你是不是感觉自己亏了?的确,亏在了带了却没用,所以伞就变成了累赘. 本节的关键就在于此,如果你定义一个变量或者对象没有被使用,那么就是不完美的代码. 我们看一个代码片段: std::string encryptPassword(const std::s

《Effective C++》之条款26:尽可能延后变量定义式的出现时间

<Effective C++> 条款26:尽可能延后变量定义式的出现时间 只要你定义了一个变量而其类型带有一个构造函数和析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本:当这个变量离开作用域时,你便得承受析构成本.即使这个变量最终并未被使用,仍需耗费这些成本,所以你应该尽量避免这种情形. 对于"尽可能延后"的理解: 不只应该延后变量多的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初始实参为止.如果这样,不仅能够避免构造(析构

Effective C++:条款26:尽可能延后变量定义式的出现时间

(一) 那么当程序的控制流到达这个变量定义时,变承受构造成本:当变量离开作用域时,便承受析构成本. string encryptPassword(const std::string& password) { using namespace std; string encrypted; if(password.length() < MinimumPasswordLengt) { throw logic_error("Password is too short") } -//

《Effective C++》学习笔记——条款26

***************************************转载请注明出处:http://blog.csdn.net/lttree******************************************** 杂谈: <Effective C++>已经看到第26个条款了,总共55条,看了也大约近一半了. 刚开始看这本书,还是因为当时想提高一下C++,然后搜了搜书想看一下, 当时不知道在哪看到了一句话: 学C++的人,分为两种读过这本书的和没读过这本书的.(话说当时还

Effective C++ Item 26 尽可能延后变量定义式的出现时间

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 经验:尽可能延后变量定义式的出现.这样做可增加程序的清晰度并改善程序效率. 示例: //这个函数过早定义变量"encrypted" std::string encryptPassword(const std::string &password){ using namespace std; string encrypted; if(password.length() <

More Effective C++ 条款35 让自己习惯于标准C++ 语言

(由于本书出版于1996年,因此当时的新特性现在来说可能已经习以为常,但现在重新了解反而会起到了解C++变迁的作用) 1. 1990年后C++的重要改变 1). 增加了新的语言特性:RTTI,namespaces,bool,关键词mutable和explicit,enums作为重载函数之自变量所引发的类型晋升转换,以及"在class 定义区内直接为整数型(intergral) const static class members设定初值"的能力. 2). 扩充了Templates的特性

More Effective C++ 条款32 在未来时态下发展程序

1. 所谓"在未来时态下发展程序",指的是是程序需要具有良好的可扩展性和可维护性,它要求程序:功能齐全,接口易用,代码泛化,以下原则有助于实现这一目标: 1). 以C++本身表现各种规范而不是仅仅依赖于注释:如果某个class不打算作为基类,那么就应该以C++语法阻止派生(条款26);如果一个class要求所有对象实体在heap内产生,就以C++语法严格厉行这项约束(条款27),如果copying和assignment对某个class没有意义,就声明为private... 2). 如果

effective c++ 条款4 make sure that objects are initialized before they are used

1 c++ 类的数据成员的初始化发生在构造函数前 class InitialData { public: int data1; int data2; InitialData(int a, int b) { data1 = a: //this is assignment data2 = b; //this is assignment } /* InitialData(int a, int b):data1(a),data2(b) //this is initial {} */ } 2 不同cpp文

More Effective C++ 条款34 如何在一个程序中结合C++和C

1. C++和C混合使用的前提之一就是编译器产生兼容的目标文件(.lib和.dll等).所谓"兼容",指的是编译器在"预编译器相依的特性上"一致,如int和double大小,参数压栈机制等,只有在这个基础上才能讨论结合使用C++和C模块的问题. 2. 在1的基础上,要结合使用C++和C的模块,主要有以下几点需要注意: 1). name mangling(名称重整) Name mangling是C++用于支持函数重载的机制,它对重载的函数名称进行一定改变,使得每个函数