《C++代码设计与重用》——2.7 转型

2.7 转型

C++代码设计与重用
2.7 转型
程序库设计者必须充分重视隐式转型(implicit conversion)。在C++中,有两种方法可以用来定义从类型From到类型To的隐式转型。第一种,我们可以在类To中定义一个只含一个参数的构造函数(并且没有其他的缺省参数):

class To {
public:
     To(const From&);  //或者是To(From)
     //...
};

或者,我们可以在类From中定义一个转型操作:

class From {
public:
     operator To()  const;
     //...
};

假如上面这两个函数中的一个(也只能是一个)存在,那么当一个类型为From的参数传递给需要类型为To(或者const To&)的参数时,就会发生隐式转型:

void f(To);
From  from;
f(from);   //发生了隐式转型

2.7.1 多重所有权(Multiple Ownership)
如果From::operator To和To(const From&)两个操作都已经声明了,那么对f的调用将会是二义性的(ambiguous):

void f(To);
//...
f(from);   //如果两个转换都定义了,就会产生二义性。
f((To) from); //仍有二义性。

增加一个强制转型(指(To)from)并不能消除二义性;但多重所有权问题(如此命名是由于类From和类To都拥有转型操作)是可以很容易避免的,只要From和To的设计者更加小心谨慎,不要提供两个转型操作就可以了。

对称转型的存在—就是说,一个从From到To的隐式转型和一个从To到From的隐式转型都存在—并不会导致相同的二义性问题。

void f(To);
void g(From);
To to;
From from;
f(from);   //发生From到To的隐式转型
g(to);   //发生To到From地隐式转型

实际上,现实的类一般都提供对称转型。考虑一个用于表述正则表达式[SM77]的类Regex,因为有必要用一个字符串来构造一个Regex对象,并且有必要把一个正则表达式解释成一个字符串,所以一个真实的Regex类提供了和String类的对称转型:

class Regex {
public:
     Regex(const String& s);
     Operator String() cosnt;
     //...
};

如果不能改变类String的定义,那么我们就只能都在类Regex内定义这两个转型操作了。

2.7.2 敏感转型
一个从From到To的隐式转型,如果它表述的是一个从From到To的自然映射,或者是用户想要默许发生的转型,我们就说这个转型是敏感的(sensible)。实际上,大多数转型都应该是敏感的(我们会在下一节讨论特殊情况)。

下面让我们来考虑几个有关敏感性(sensibility)的例子。假设我们是一个数学程序库的设计者,在我们的程序库里,类Rational和类Complex分别描述有理数和复数。假如我们只考虑int、double、Rational和Complex 4种类型,就会有12种可能的隐式转型;那么,这些转型中哪些是敏感的呢?

Complex到int的转型
很显然,这个转型不是敏感的(sensible)。尽管我们有可能定义任何从复数到整数的映射,但这种定义肯定不是自然的。

int到Complex的转型
这是一个很明显的映射:从整数x映射到复数x+(0)(),并且,用户大多数都希望整数可以默认地转化为复数。因此,这个转型是敏感的。

Rational到double的转型
将一个Rational对象映射到一个double对象,这个double对象储存的值是Rational对象的分子除以分母得到的近似值,整个过程将可能损失一定的精度。尽管这个转型有时可以被认为是自然的,但是用户往往不想让这种转型默认地进行(因为精度可能损失)。因此,这种转型不是敏感的。

double到Rational的转型
如果我们认为double描述的是实数集合,那么从double到Rational将没有自然的映射;但实际上,每个double对象表述的是一个有限小数,并且有限小数和有理数有着很好的自然映射,因此,这个转型有可能是敏感的。

Complex到Rational的转型
由于不存在用户希望发生的、从Complex到Rational的自然映射,所以这个转型不是敏感的。

Rational到Complex的转型
这个转型是比较复杂的。每一个有理数同时也可以是一个复数,因此我们可以认为存在一个从Rational到Complex的映射。然而,如果Complex只能以x+(y)()的形式来表示复数,其中x和y都是double类型,那么这个转型就不是敏感的了,原因和Rational到double的转型不是敏感的原因相同(即精度损失)。

从上面这些例子可以看出,经过了详细的分析,很多隐式转型都不是敏感的,因此,我们就不应该提供这类转型操作。练习2.6要求读者判断剩余转型的敏感性。

注意,对于某些非敏感的隐式转型,如果我们的程序确实需要它所实现的功能,那么我们可以把这种转型实现为显式转型。例如,尽管从Rational到double的隐式转型不是敏感的,并且会有精度的损失,但用户却可以显式地(explicitly)将Rational对象转型为double对象;我们可以在数学程序库里提供这个函数:

    class Rational {
    public:
         double to_double() const;
         //...
    };

2.7.3 不敏感转型
回想一下2.4.1节的Pool类,如2.4.1节所述,Pool的构造函数需要包含一个大小参数:

    Pool::Pool(size_t n) {/* ... */};

此外,我们也没有给这个函数传递其他的参数。因此,我们会得到一个单参数的构造函数,并由它来创建一个转型函数。遗憾的是,从size_t到Pool的转型并不是敏感的—几乎没有用户会把一个size_t对象默认地转化为一个Pool对象:

    void f(const Pool& p);
    //...
    f(17); //合法,但无疑是错误的{![译注:实际上C++标准中的一个新关键字explieit已经很好地解决了这个问题。]}

我们可以用两种方法来解决上面这个问题。第一种,我们可以只提供构造函数,把避免隐式转型的工作留给用户完成;第二种,我们可以定义一个中间类:

    class Pool {
    public:
         class Size {
         public:
              Size(size_t n);
              //...
         };
         pool(Size n);
         //...
    };

因为对函数的实参,如果它的转型是用户定义的,那么C++是不能(隐式地)执行多于一次的转型的;这个设计将会导致编译器拒绝编译下面的错误代码:

    void f(const Pool&);
    //...
    f(17); //错误

然而,定义一个中间类有很大的缺点:它使Pool类的理解和使用更加困难。现在如果想要构造一个Pool对象,用户应该这样编码:

    Pool p(Pool::Size(17));

大多数用户都会倾向于使用虽容易产生错误,但更加简单的接口;因此,程序库有时也提供不敏感的(nonsensible)转型。

2.7.4 转型数目(Fanout)

我们可以这样来定义一个类型的转型数目(fanout):它就是这个类型可以隐式转换为其他类型的数目。很大的转型数目往往不是我们所期望的,因为它很容易导致二义性的发生。例如,假设类From可以转型为两种类型:To和Another_to,那么下面的函数调用就存在二义性:

void f(To);
        void f(Another_to);
        From from;
        f(from);   //二义性

增加一个强制转型(cast)就可以解决这个二义性问题:

f((To) from);   //OK, 发生了从From到To的转型

然而,如果可能的话,C++程序库不应该强迫它的用户在他们的代码中使用强制转型(cast)。因此,C++程序库应该避免大的转型数目。幸运的是,只提供敏感转型的程序库一般只具有小的转型数目,特殊情况就是一些诸如int和char 的内建类型;因为很多类定义了具有这种类型单参数的构造函数,于是int和char 就具有很大的转型数目。为了避免二义性问题,程序库用户无论在什么时候,都应该避免依赖从内建类型到程序库定义类型之间的隐式转型。

本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

时间: 2024-08-21 11:46:32

《C++代码设计与重用》——2.7 转型的相关文章

《C++代码设计与重用》导读

前言 C++代码设计与重用 一切事物都将得到检验并因此被称为问题. Edith Hamilton 这本书的主要目的在于:展示如何以C++编程语言编写可重用代码-就是说,根据不同的需要,在不经过修改,或者经过很少修改的前提下,可重用代码可以很容易地应用到5个.50个甚至500个程序当中,而且这些程序往往是不同程序员编写的,可能运行在不同的系统上.在整个阐述的过程中,我们的目的并不在于争论是否所有的代码都是可重用的,也不在于说明可重用代码能够解决所有的程序问题.显然,不论是对程序员而言,还是对可重用

《C++代码设计与重用》——2.3 Nice类

2.3 Nice类 C++代码设计与重用 2.3 Nice类 我们都知道类会提供某些函数,这些函数要么是在类的代码中被显式声明为公共的(public)或保护的(protected),要么是由编译器在程序需要这些代码时隐式生成的.例如,下面这个类: class X{ public: X(); void f(); }; 它提供了一个缺省构造函数.函数f.一个拷贝构造函数.一个赋值运算符和一个析构函数.而且最后3个函数会在程序需要它们的时候由编译器自动生成. 请考虑下面这个通常有用的函数: templ

《C++代码设计与重用》——2.9 总结

2.9 总结 C++代码设计与重用2.9 总结正规函数-拷贝构造函奴.析构函数.基本赋值运算符.相等运算符和不等运算符-在所有的类中都应该实现相同的语义. 尽管没有最小标准接口,但是nice函数-缺省构造函数.拷贝构造函数.赋值运算符和相等运算符-应该是大多数类都提供的函数.没有任何函数是所有的类都应该提供的函数:而且,绝大多数类都不应该提供浅拷贝和深拷贝操作. 对程序库中类的接口一致性,我们应该给予充分的重视.但是当一致性使类的接口变得很不适当或者不直观时,我们就不能一味顽固地坚持这种一致性.

《C++代码设计与重用》——2.5 浅拷贝和深拷贝

2.5 浅拷贝和深拷贝 C++代码设计与重用2.5 浅拷贝和深拷贝有两个操作,尽管它们具有某些不合乎需要的特性,但因为它们的使用范围很广,进而博得一定的注意,所以这两个操作在这里有必要特别提及一下,这两个操作就是浅拷贝操作和深拷贝操作.x对象的浅拷贝是指:另一个和x相同类型的,并且它的数据成员和x相对应的数据成员具有相同值的对象.x对象的深拷贝是指:另一个和x类型相同的对象,它具有x直接或间接指向的对象的一份拷贝,并且在拷贝里,所有共享和循环的联系依旧保留.考虑下面3个类: class Z {

《C++代码设计与重用》——2.8 const关键字的使用

2.8 const关键字的使用 C++代码设计与重用2.8 const关键字的使用在程序库中,const关键字的正确使用是很重要的.使用const的最大障碍就是用户往往未能正确理解const的意义.接下来,我们将讨论如何解释const,如何使用const,和当我们要改变const的时候,const为什么不能够被重新解释. 2.8.1 抽象const对比位元const 我们可以用好几种方式来解释关键字const.先考虑函数sqrt,它用于计算Rational对象的平方根(这里的Rational指2

《C++代码设计与重用》——2.10 练习

2.10 练习 C++代码设计与重用2.10 练习2.1 给出下面被建议为最小标准接口函数的反例: a.输入函数: b.输出函数: c.用字符串返回外层类类名的函数. 2.2 考虑类WORM_Pool,它和2.4.1节的Pool类很相似,但这一点除外,它在只能写一次但可读多次的内存区域分配内存块.那么,类WORD_Pool是析构函数的反例吗?请说明是或不是的原因. 2.3 假设我们为用户提供一个类Buf,它描述一个缓冲区: class Buf { public: Buf(size_t sz);

《C++代码设计与重用》——1.5 这本书能给我们带来什么

1.5 这本书能给我们带来什么 C++代码设计与重用 1.5 这本书能给我们带来什么 编写可重用代码可以使复杂的问题变得比较简单,但编码过程是非常困难的.这本书不会也不能让这困难的过程变得格外简单,这本书也没有提供能让每个C++程序员都可以很轻松地编写出可重用代码的锦囊妙计. 针对每个希望编写出可重用代码的C++程序员,这本书的每一章都讨论了一个或者多个他们必须理解的问题.理解了这些问题虽然不能使编写可重用代码变得相当简单,但可以让编写出可重用代码成为一种可能. 这本书的其余部分的结构如下: 当

《C++代码设计与重用》——1.2 重用的神话

1.2 重用的神话 C++代码设计与重用1.2 重用的神话关于代码重用出现了许多神话(荒诞的说法),这一节我们来反驳几个比较普遍的说法. 神话1:重用可以解决软件危机 软件危机是指程序设计团体现今没有能力做到以下几点:编写解决复杂问题的程序,快速生成解决复杂问题的程序,正确编写这些程序并使这些程序的维护相当容易. 软件开发进步的迹象是显而易见的.一个很显然的迹象就是随着时间的推移,所谓的复杂问题的范围发生了改变.在20世纪60年代,编写一个FORTRAN-66编译器就被认为是一个非常复杂的问题:

《C++代码设计与重用》——1.1 什么是重用性

1.1 什么是重用性 C++代码设计与重用 1.1 什么是重用性 许多相同操作都会在多个计算机程序里重复实现,例如: 对数组元素进行排序:解答线性方程组:实现一个从X类型到Y类型的映射:解析C++代码:从数据库检索数据:和其他程序进行通信.与其在每个程序里都设计和实现上面每个操作的相同代码,我们更愿意采用的方法是:只设计和实现这些操作的代码一次,然后再把这些代码重用手不同程序里.显然,已有的可重用代码,使每个应用程序不必从头写起,因为它(可重用代码)大大加速了应用程序的开发,并且减少了编写和维护