《C++代码设计与重用》——2.4 存在最小标准接口吗

2.4 存在最小标准接口吗

C++代码设计与重用
2.4 存在最小标准接口吗
一些专家(如[RC90]里的Riel和Carter)在对类进行深入研究之后,主张所有的类都应该提供某个最小标准接口。但究竟应该提供哪种最小标准接口呢?不同专家的建议往往又大相径庭。所有建议的标准接口除了包含nice函数(指nice类提供的函数)外,还包含诸如输入输出函数、哈希函数、以字符串返回类名的函数、浅拷贝和深拷贝操作等。

即使提供某个最小标准接口的动机是好的,但如果试图对所有的类都定义这个最小标准接口,那就是很不可取的。没有任何一个函数是所有类都必须提供的,下面的论据就说明了这个观点:对任何建议的某个最小标准接口函数,肯定会描述出某个类,它根本不需要提供这个函数。

如果某个类是最小标准接口函数的反例,通常是由于以下3种原因中的一种:未能给出函数切合实际的语义,函数可能没有实现的价值,或者,就算函数刻意实现,但它带来的坏处往往多于好处。

在这里,由于篇幅的限制,我们不可能给出每个被建议为最小标准接口函数的反例,但我们将给出析构函数以外的所有nice函数的反例。由上可知,如果可以给出这些函数的反例,我们就有充分的理由相信:除了析构函数之外,其他所有函数都有不能作为最小标准接口的函数。

2.4.1 缺省构造函数

考虑下面具有特殊用途的内存配置器(allocator):

class Pool {
    public:
         Pool(size_t n);
         void* alloc();
         void free(void* p);
         //...
    };

如上所示,Pool(n)构造函数高效地分配和回收n个字节单位的内存块;Pool::alloc函数用来返回一个指针,这个指针指向一块至少含有n个字节的连续空白内存块。对同一个Pool对象q,如果p是由q通过调用alloc函数返回的指针,那么调用q.frec(p)将会把p指向的内存块释放并返回给对象q1。于是,Pool::free函数的合理实现将是有前提条件的,这就是,它的参数的值(也就是指针指向的对象)必须是同一个Pool对象早先通过调用alloc函数的返回值。

但如果我们给Pool提供一个缺省构造函数,情况又会是什么样呢?

class Pool{
    public:
         Pool();
         //...
    };

由上可知,Pool的缺省构造函数必须创建一个可以高效分配和回收n个单位内存的对象,而不管这个n的值是多少。因为在缺省构造函数中,用户并没有给出n的值,所以如果我们(Pool类的设计者)必须决定应该对它分配多少内存块,又该使用哪一个具体的n值?

随机地选择一个数值来作为n值,譬如37,显然是错误的。那么,我们或许会考虑选择0或者1。然而,用户不太可能要求分配0或1个字节的内存。实际上,创建一个分配0或1个单位内存的Pool对象很可能会导致逻辑错误。这样,为类Pool提供一个缺省构造函数将会导致运行时错误,而如果不提供缺省构造函数的话,这种错误(指创建一个分配0或1个单元内存的Pool对象导致的错误)应该在编译时就可以检测到的。因为用户总是希望可以尽早地检测到错误,所以我们就不应该给Pool类提供一个缺省构造函数。因此,对于那种认为可以把缺省构造函数包括到最小标准接口之中的论断,Pool类就是它的反例。

2.4.2 赋值运算符

假设我们给Pool类提供一个赋值运算符:

class Pool {
    public:
         const Pool& operator=(const Pool& q);
         //...
    };

赋值的正规语义(semantic)表明,这个函数把被赋的Pool对象转化为Pool(n)对象,其中n是q对象分配的内存块的字节数,例如:

Pool p(4);     //p分配了4个字节的内存块
Pool q(8);     //q分配了8个字节的内存块
P=q;       //p现在分配了8个字节的内存块

再考虑下面企图给一个Pool对象赋值的代码:

Pool p(4);
void* mem = p.alloc();
Pool q(8);
p=q;

在这里,用户从Pool p对象分配内存空间,并用mem储存p.alloc函数的返回指针,然后把Pool q对象赋值给Pool p对象。然而,Pool p对象和Pool q对象分配了不同字节数的内存块,因此当用户最后想要释放mem指向的内存块时,

p.free(mem);

在Poo1的任何实际实现中,混乱2很可能就会接踵而至。

我们可以通过给定Pool类赋值运算符的前提条件来避免上面这个问题;这个前提条件就是,从赋值的Pool对象和被赋值的Pool对象中分配出长度相同的内存块3。另一个前提条件是,我们可以要求被赋值的Pool对象(如p)此时不分配任何内存块4。但请注意,赋值运算符的正规语义(见2.2节)并没有强加这些前提条件。如果我们决定强加这些前提条件,那么第二个条件的检查将会相对容易一些。因为对于第二个前提条件,我们可以通过比较free函数调用的次数和alloc函数调用的次数来获知条件是否成立;如果这两个调用次数相等,那么目前就没有已经被分配的内存块,就是说条件成立。

但为什么会如此麻烦呢?因为一个诸如Pool的类往往被许多C++程序员加到他们的工具箱里面,而且经验也显示程序员并不需要类Pool的赋值运算符操作。进一步说,提供Pool::operator=还会使那些应该在编译时检测到的错误延迟到运行时才出现。因此,类Pool就是把赋值运算符包含到最小标准接口的反例。

2.4.3 拷贝构造函数

现在考虑拷贝构造函数。假设我们正在设计一个类Parser,用来描述C++解析器。然而,Parser将会是非常复杂的—1个Parser对象要包括符号表和许多重要的内部数据结构。因此,用正确的(正规的)语义来给这样一个复杂对象实现拷贝构造函数将会是非常乏味、浪费时间和容易产生错误的。此外,用户几乎不愿意拷贝如此大的Parser对象。

对一个用户不需要的拷贝构造函数,如果我们花费宝贵的肘间来设计、实现和测试它,那将会是毫无意义的。因此,对那种认为拷贝构造函数可以放到最小标准接口之中的论断,Parser类就是一个很好的反例。

2.4.4 相等运算符

对某些类,定义和实现一个相等运算符会是很困难的。考虑两个Parser对象,即使它们的底层表述5具有不同的值,但它们的(抽象)值却可能是相同的(因此它们的比较结果相等)。例如,两个具有相同(抽象)值的Parser对象可能指向不同的符号表对象,不同的符号表对象指向不同的符号对象,而不同的符号对象又指向不同类型的对象,不同类型的对象又指向类型名不同的对象,依此类推。

由上可知,为了实现和测试一个Parser类的相等运算符,我们需要花费巨大的努力;然而,用户却很少想要比较Parser对象。因此,为Parse类实现相等运算符将会是毫无意义的。

2.4.5 析构函数

最后,我们来考虑析构函数。如前所述,对于任何需要析构函数而没有显式定义析构函数的类,编译器都会为它创建一个析构函数。因此,如果想找出析构函数的反例的话,我们就必须把析构函数显式声明为私有(Private)函数。

class T {
private:
      ~T();
      //...
};

(如果类的实现并不需要析构函数的话,我们就没有必要定义析构函数。)

声明了私有析构函数的类T将会是什么样的呢?类T的用户不能在静态储存器或堆栈上创建T对象:

T t;   //错误:不能存取private T::~T()
void f() {
     T t;  //错误:不能存取private T::~T()
     //...
}

因此,用户只能在动态存储器上创建T对象;但是,用户不能删除这些被创建的T对象:

T* t = new T;
//...
delete t; //错误:不能存取private T::~T()

希望具备这种特性(把析构函数声明为私有函数)的类是很少的,但却是实实在在存在的。假设用户运行环境具有垃圾收集器,并且由于某种原因我们要禁止手动删除X类型的对象,那么只要用户不需要在静态存储器和堆栈里创建X对象,我们就可以把类X的析构函数声明为私有函数。

1译注:由于p指向的内存区域本身就是从内存配置对象q中分配出来的,q可以是预先分配好的很大一个内存块,所以这里释放p指向的内存区域,回收给内存配置对象q。
2译注:因为p的物理内存已经发生改变,mem现在不一定属于P内存区域以内了。
3译注:即(如p和q)都分配mem指向的内存块大小的内存。
4译注:即p被赋值后是一块含有8个单位的内存块,没有为mem所指对象分配内存。
5译注:指实际代表的值。底层表述的值,在这里大概可以说成是类所包含的属于内置类型的成员变量的值。其他变量,直到递归转化为内置类型的成员变量为止。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

时间: 2024-09-04 06:00:46

《C++代码设计与重用》——2.4 存在最小标准接口吗的相关文章

《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++代码设计与重用》——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++代码:从数据库检索数据:和其他程序进行通信.与其在每个程序里都设计和实现上面每个操作的相同代码,我们更愿意采用的方法是:只设计和实现这些操作的代码一次,然后再把这些代码重用手不同程序里.显然,已有的可重用代码,使每个应用程序不必从头写起,因为它(可重用代码)大大加速了应用程序的开发,并且减少了编写和维护

《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: operato

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

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

《C++代码设计与重用》——2.2 正规函数

2.2 正规函数 C++代码设计与重用2.2 正规函数对所有提供它们的类而言,某些函数应该具有相同的语义.考虑类Rational的拷贝构造函数: class Rational { public: Rational(const Rational& r); //... }; 上面的操作将会构造一个Rational对象,它的值等同于对象r的值(我们所说的值总是指抽象值).我们认为,每个类的拷贝构造函数都应该具有这样的语义,就是构造一个和它的参数等值的对象.尽管C++没有-也不能-强制拷贝构造函数遵循这

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

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