Team LiB
Previous Section Next Section

15.8. Handle Classes and Inheritance

15.8. 句柄类与继承

One of the ironies of object-oriented programming in C++ is that we cannot use objects to support it. Instead, we must use pointers and references, not objects. For example, in the following code fragment,

C++ 中面向对象编程的一个颇具讽刺意味的地方是,不能使用对象支持面向对象编程,相反,必须使用指针或引用。例如,下面的代码段中:

     void get_prices(Item_base object,
                     const Item_base *pointer,
                     const Item_base &reference)
     {
         // which version of net_price is called is determined at run time
         cout << pointer->net_price(1) << endl;
         cout << reference.net_price(1) << endl;

         // always invokes Item_base::net_price
         cout << object.net_price(1) << endl;
     }

the invocations through pointer and reference are resolved at run time based on the dynamic types of the object to which they are bound.

通过 pointerreference 进行的调用在运行时根据它们所绑定对象的动态类型而确定。

Unfortunately, using pointers or references puts a burden on the users of our classes. We saw one such burden in the previous section that discussed the inter-actions between objects of inherited types and containers.

但是,使用指针或引用会加重类用户的负担。在前一节中讨论继承类型对象与容器的相互作用时,已经碰到了一种这样的负担。

A common technique in C++ is to define a so-called cover or handle class. The handle class stores and manages a pointer to the base class. The type of the object to which that pointer points will vary; it can point at either a base- or a derived-type object. Users access the operations of the inheritance hierarchy through the handle. Because the handle uses its pointer to execute those operations, the behavior of virtual members will vary at run time depending on the kind of object to which the handle is actually bound. Users of the handle thus obtain dynamic behavior but do not themselves have to worry about managing the pointer.

C++ 中一个通用的技术是定义包装(cover)类或句柄。句柄类存储和管理基类指针。指针所指对象的类型可以变化,它既可以指向基类类型对象又可以指向派生类型对象。用户通过句柄类访问继承层次的操作。因为句柄类使用指针执行操作,虚成员的行为将在运行时根据句柄实际绑定的对象的类型而变化。因此,句柄的用户可以获得动态行为但无须操心指针的管理。

Handles that cover an inheritance hierarchy have two important design considerations:

包装了继承层次的句柄有两个重要的设计考虑因素:

  • As with any class that holds a pointer (Section 13.5, p. 492), we must decide what to do about copy control. Handles that cover an inheritance hierarchy typically behave like either a smart pointer (Section 13.5.1, p. 495) or a value (Section 13.5.2, p. 499).

    像对任何保存指针(第 13.5 节)的类一样,必须确定对复制控制做些什么。包装了继承层次的句柄通常表现得像一个智能指针(第 13.5.1 节)或者像一个值(第 13.5.2 节)。

  • The handle class determines whether the handle interface will hide the inheritance hierarchy or expose it. If the hierarchy is not hidden, users must know about and use objects in the underlying hierarchy.

    句柄类决定句柄接口屏蔽还是不屏蔽继承层次,如果不屏蔽继承层次,用户必须了解和使用基本层次中的对象。

There is no one right choice among these options; the decisions depend on the details of the hierarchy and how the class designer wants programmers to interact with those class(es). In the next two sections, we'll implement two different kinds of handles that address these design questions in different ways.

对于这些选项没有正确的选择,决定取决于继承层次的细节,以及类设计者希望程序员如何与那些类相互作用。下面两节将实现两种不同的句柄,用不同的方式解决这些设计问题。

15.8.1. A Pointerlike Handle

15.8.1. 指针型句柄

As our first example, we'll define a pointerlike handle class, named Sales_item, to represent our Item_base hierarchy. Users of Sales_item will use it as if it were a pointer: Users will bind a Sales_item to an object of type Item_base and will then use the * and -> operations to execute Item_base operations:

像第一个例子一样,我们将定义一个名为 Sales_item 的指针型句柄类,表示 Item_base 层次。Sales_item 的用户将像使用指针一样使用它:用户将 Sales_item 绑定到 Item_base 类型的对象并使用 *-> 操作符执行 Item_base 的操作:

     // bind a handle to a Bulk_item object
     Sales_item item(Bulk_item("0-201-82470-1", 35, 3, .20));

     item->net_price();   // virtual call to net_price function

However, users won't have to manage the object to which the handle points; the Sales_item class will do that part of the job. When users call a function through a Sales_item, they'll get polymorphic behavior.

但是,用户不必管理句柄指向的对象,Sales_item 类将完成这部分工作。当用户通过 Sales_item 类对象调用函数时,将获得多态行为。

Defining the Handle
定义句柄

We'll give our class three constructors: a default constructor, a copy constructor, and a constructor that takes an Item_base. This third constructor will copy the Item_base and ensure that the copy stays around as long as the Sales_item does. When we copy or assign a Sales_item, we'll copy the pointer rather than copying the object. As with our other pointerlike handle classes, we'll use a use count to manage the copies.

Sales_item 类有三个构造函数:默认构造函数、复制构造函数和接受 Item_base 对象的构造函数。第三个构造函数将复制 Item_base 对象,并保证:只要 Sales_item 对象存在副本就存在。当复制 Sales_item 对象或给 Sales_item 对象赋值时,将复制指针而不是复制对象。像对其他指针型句柄类一样,将用使用计数来管理副本。

The use-counted classes we've used so far have used a companion class to store the pointer and associated use count. In this class, we'll use a different design, as illustrated in Figure 15.2. The Sales_item class will have two data members, both of which are pointers: One pointer will point to the Item_base object and the other will point to the use count. The Item_base pointer might point to an Item_base object or an object of a type derived from Item_base. By pointing to the use count, multiple Sales_item objects can share the same counter.

迄今为止,我们已经使用过的使用计数式类,都使用一个伙伴类来存储指针和相关的使用计数。这个例子将使用不同的设计,如图 15.2 所示。Sales_item 类将有两个数据成员,都是指针:一个指针将指向 Item_base 对象,而另一个将指向使用计数。Item_base 指针可以指向 Item_base 对象也可以指向 Item_base 派生类型的对象。通过指向使用计数,多个 Sales_item 对象可以共享同一计数器。

Figure 15.2. Use-Count Strategy for the Sales_item Handle Class
图 15.2. Sales_item 句柄类的使用计数策略


In addition to managing the use count, the Sales_item class will define the dereference and arrow operators:

除了管理使用计数之外,Sales_item 类还将定义解引用操作符和箭头操作符:

     // use counted handle class for the Item_base hierarchy
     class Sales_item {
     public:
         // default constructor: unbound handle
         Sales_item(): p(0), use(new std::size_t(1)) { }
         // attaches a handle to a copy of the Item_base object
         Sales_item(const Item_base&);
         // copy control members to manage the use count and pointers
         Sales_item(const Sales_item &i):
                           p(i.p), use(i.use) { ++*use; }
         ~Sales_item() { decr_use(); }
         Sales_item& operator=(const Sales_item&);
         // member access operators
         const Item_base *operator->() const { if (p) return p;
             else throw std::logic_error("unbound Sales_item"); }
         const Item_base &operator*() const { if (p) return *p;
             else throw std::logic_error("unbound Sales_item"); }
     private:
         Item_base *p;        // pointer to shared item
         std::size_t *use;    // pointer to shared use count
         // called by both destructor and assignment operator to free pointers
         void decr_use()
              { if (--*use == 0) { delete p; delete use; } }
     };

Use-Counted Copy Control
使用计数式复制控制

The copy-control members manipulate the use count and the Item_base pointer as appropriate. Copying a Sales_item involves copying the two pointers and incrementing the use count. The destructor decrements the use count and destroys the pointers if the count goes to zero. Because the assignment operator will need to do the same work, we implement the destructor's actions in a private utility function named decr_use.

复制控制成员适当地操纵使用计数和 Item_base 指针。复制 Sales_item 对象包括复制两个指针和将使用计数加 1.析构函数将使用计数减 1,如果计数减至 0 就撤销指针。因为赋值操作符需要完成同样的工作,所以在一个名为 decr_use 的私有实用函数中实现析构函数的行为。

The assignment operator is a bit more complicated than the copy constructor:

赋值操作符比复制构造函数复杂一点:

     // use-counted assignment operator; use is a pointer to a shared use count
     Sales_item&
     Sales_item::operator=(const Sales_item &rhs)
     {
         ++*rhs.use;
         decr_use();
         p = rhs.p;
         use = rhs.use;
         return *this;
     }

The assignment operator acts like the copy constructor in that it increments the use count of the right-hand operand and copies the pointer. It also acts like the destructor in that we first have to decrement the use count of the left-hand operand and then delete the pointers if the use count goes to zero.

赋值操作符像复制构造函数一样,将右操作数的使用计数加 1 并复制指针;它也像析构函数一样,首先必须将左操作数的使用计数减 1,如果使用计数减至 0 就删除指针。

As usual with an assignment operator, we must protect against self-assignment. This operator handles self-assignment by first incrementing the use count in the right-hand operand. If the left- and right-hand operands are the same, the use count will be at least 2 when decr_use is called. That function decrements and checks the use count of the left-hand operand. If the use count goes to zero, then decr_use will free the Item_base and use objects currently in this object. What remains is to copy the pointers from the right-hand to the left-hand operand. As usual, our assignment operator returns a reference to the left-hand operand.

像通常对赋值操作符一样,必须防止自身赋值。这个操作符通过首先将右操作数的使用计数减 1 来处理自身赋值。如果左右操作数相同,则调用 decr_use 时使用计数将至少为 2。该函数将左操作数的使用计数减 1 并进行检查,如果使用计数减至 0,则 decr_use 将释放该对象中的 Item_base 对象和 use 对象。剩下的是从右操作数向左操作数复制指针,像平常一样,我们的赋值操作符返回左操作数的引用。

Aside from the copy-control members, the only other functions Sales_item defines are the operator functions, operator* and operator->. Users will access Item_base members through these operators. Because these operators return a pointer and reference, respectively, functions called through these operators will be dynamically bound.

除了复制控制成员以外,Sales_item 定义的其他函数是是操作函数 operator*operator->,用户将通过这些操作符访问 Item_base 成员。因为这两个操作符分别返回指针和引用,所以通过这些操作符调用的函数将进行动态绑定。

We define only the const versions of these operators because the public members in the underlying Item_base hierarchy are all const.

我们只定义了这些操作符的 const 版本,因为基础 Item_base 层次中的成员都是 const 成员。

Constructing the Handle
构造句柄

Our handle has two constructors: the default constructor, which creates an un-bound Sales_item, and a second constructor, which takes an object to which it attaches the handle.

我们句柄有两个构造函数:默认构造函数创建未绑定的 Sales_item 对象,第二个构造函数接受一个对象,将句柄与其关联。

The first constructor is easy: We set the Item_base pointer to 0 to indicate that this handle is not attached to any object. The constructor allocates a new use counter and initializes it to 1.

第一个构造函数容易定义:将 Item_base 指针置 0 以指出该句柄没有关联任何对象上。构造函数分配一个新的计数器并将它初始化为 1。

The second constructor is more difficult. We'd like users of our handle to create their own objects, to which they could attach a handle. The constructor will allocate a new object of the appropriate type and copy the parameter into that newly allocated object. That way the Sales_item class will own the object and can guarantee that the object is not deleted until the last Sales_item attached to the object goes away.

第二个构造函数难一点,我们希望句柄的用户创建自己的对象,在这些对象上关联句柄。构造函数将分配适当类型的新对象并将形参复制到新分配的对象中,这样,Sales_item 类将拥有对象并能够保证在关联到该对象的最后一个 Sales_item 对象消失之前不会删除对象。

15.8.2. Cloning an Unknown Type

15.8.2. 复制未知类型

To implement the constructor that takes an Item_base, we must first solve a problem: We do not know the actual type of the object that the constructor is given. We know that it is an Item_base or an object of a type derived from Item_base. Handle classes often need to allocate a new copy of an existing object without knowing the precise type of the object. Our Sales_item constructor is a good example.

要实现接受 Item_base 对象的构造函数,必须首先解决一个问题:我们不知道给予构造函数的对象的实际类型。我们知道它是一个 Item_base 对象或者是一个 Item_base 派生类型的对象。句柄类经常需要在不知道对象的确切类型时分配书籍对象的新副本。Sales_item 构造函数是个好例子。

The common approach to solving this problem is to define a virtual operation to do the copy, which we'll name clone.

解决这个问题的通用方法是定义虚操作进行复制,我们称将该操作命名为 clone



To support our handle class, we'll need to add clone to each of the types in the hierarchy, starting with the base class, which must define the function as virtual:

为了句柄类,需要从基类开始,在继承层次的每个类型中增加 clone,基类必须将该函数定义为虚函数:

     class Item_base {
     public:
         virtual Item_base* clone() const
                            { return new Item_base(*this); }
     };

Each class must now redefine the virtual. Because the function exists to generate a new copy of an object of the class, we'll define the return type to reflect the type of the class itself:

每个类必须重定义该虚函数。因为函数的存在是为了生成类对象的新副本,所以定义返回类型为类本身:

     class Bulk_item : public Item_base {
     public:
         Bulk_item* clone() const
             { return new Bulk_item(*this); }
     };

On page 564 we said there is one exception to the requirement that the return type of the derived class must match exactly that of the base class instance. That exception supports cases such as this one. If the base instance of a virtual function returns a reference or pointer to a class type, the derived version of the virtual may return a class publicly derived from the class returned by the base class instance (or a pointer or a reference to a class type).

第 15.2.3 节介绍过,对于派生类的返回类型必须与基类实例的返回类型完全匹配的要求,但有一个例外。这个例外支持像这个类这样的情况。如果虚函数的基类实例返回类类型的引用或指针,则该虚函数的派生类实例可以返回基类实例返回的类型的派生类(或者是类类型的指针或引用)。

Defining the Handle Constructors
定义句柄构造函数

Once the clone function exists, we can write the Sales_item constructor:

一旦有了 clone 函数,就可以这样编写 Sales_item 构造函数:

     Sales_item::Sales_item(const Item_base &item):
                 p(item.clone()), use(new std::size_t(1)) { }

Like the default constructor, this constructor allocates and initializes its use count. It calls clone on its parameter to generate a (virtual) copy of that object. If the argument is an Item_base, then the clone function for Item_base is run; if the argument is a Bulk_item, then the Bulk_item clone is executed.

像默认构造函数一样,这个构造函数分配并初始化使用计数,它调用形参的 clone 产生那个对象的(虚)副本。如果实参是 Item_base 对象,则运行 Item_baseclone 函数;如果实参是 Bulk_item 对象,则执行 Bulk_itemclone 函数。

Exercises Section 15.8.2

Exercise 15.31:

Define and implement the clone operation for the limited discount class implemented in the exercises for Section 15.2.3 (p. 567).

第 15.2.3 节的习题中实现的有限折扣类定义的实现 clone 操作。

Exercise 15.32:

In practice, our programs are unlikely to run correctly the first time we run them or the first time we run them against real data. It is often useful to incorporate a debugging strategy into the design of our classes. Implement a virtual debug function for our Item_base class hierarchy that displays the data members of the respective classes.

实际上,程序不太可能在第一次运行或第一次用真实数据运行时就能正确运行。在类的设计中包括调试策略经常是有用的。为 Item_base 类层次实现一个 debug 虚函数,显示各个类的数据成员。

Exercise 15.33:

Given the version of the Item_base hierarchy that includes the Disc_item abstract base class, indicate whether the Disc_item class should implement the clone function. If not, why not? If so, why?

对于 Item_base 层次的包括 Disc_item 抽象基类的版本,指出 Disc_item 类是否应实现 clone 函数,为什么?

Exercise 15.34:

Modify your debug function to let users turn debugging on or off. Implement the control two ways:

修改调试函数以允许用户打开或关闭调试。用两种方式实现控制:

  1. By defining a parameter to the debug function

    通过定义 debug 函数的形参。

  2. By defining a class data member that allows individual objects to turn on or turn off the display of debugging information

    通过定义类数据成员。该成员允许个体对象打开或关闭调试信息的显示。


15.8.3. Using the Handle

15.8.3. 句柄的使用

Using Sales_item objects, we could more easily write our bookstore application. Our code wouldn't need to manage pointers to the Item_base objects, yet the code would obtain virtual behavior on calls made through a Sales_item.

使用 Sales_item 对象可以更容易地编写书店应用程序。代码将不必管理 Item_base 对象的指针,但仍然可以获得通过 Sales_item 对象进行的调用的虚行为。

As an example, we could use Item_base objects to solve the problem proposed in Section 15.7 (p. 597). We could use Sales_items to keep track of the purchases a customer makes, storing a Sales_item representing each purchase in a multiset. When the customer was done shopping, we would total the sale.

例如,可以使用 Item_base 对象解决第 15.7 节提出的问题。可以使用 Sales_item 对象跟踪顾客所做购买,在 multiset 中保存一个对象表示一次购买,当顾客完成购买时,可以计算销售总数。

Comparing Two Sales_items
比较两个 Sales_item 对象

Before writing the function to total a sale, we need to define a way to compare Sales_items. To use Sales_item as the key in an associative container, we must be able to compare them (Section 10.3.1, p. 360). By default, the associative containers use the less-than operator on the key type. However, for the same reasons discussed about our original Sales_item type in Section 14.3.2 (p. 520), defining operator< for the Sales_item handle would be a bad idea: We want to take only the ISBN into account when we use Sales_item as a key, but want to consider all data members when determining equality.

在编写函数计算销售总数之前,需要定义比较 Sales_item 对象的方法。要用 Sales_item 作为关联容器的关键字,必须能够比较它们(第 10.3.1 节)。关联容器默认使用关键字类型的小于操作符,但是,基于第 14.3.2 节讨论过的有关原始 Sales_item 类型的同样理由,为 Sales_item 句柄类定义 operator> 可能是个坏主意:当使用 Sales_item 作关键字时,只想考虑 ISBN,但确定相等时又想要考虑所有数据成员。

Fortunately, the associative containers allow us to specify a function (or function object (Section 14.8, p. 530)) to use as the comparison function. We do so similarly to the way we passed a separate function to the stable_sort algorithm in Section 11.2.3 (p. 403). In that case, we needed only to pass an additional argument to stable_sort to provide a comparison function to use in place of the < operator. Overriding an associative container's comparison function is a bit more complicated because, as we shall see, we must supply the comparison function when we define the container object.

幸好,关联容器使我们能够指定一个函数或函数对象(第 14.8 节)用作比较函数,这样做类似于第 11.2.3 节中将单独函数传给 stable_sort 算法的方式。在那种情况下,只需要将附加的实参传给 stable_sort 以提供比较函数,代替 < 操作符的。覆盖关联容器的比较函数有点复杂,因为,正如我们将看到的,在定义容器对象时必须提供比较函数。

Let's start with the easy part, which is to define a function to use to compare Sales_item objects:

让我们比较容易的部分开始,定义一个函数用于比较 Sales_item 对象:

     // compare defines item ordering for the multiset in Basket
     inline bool
     compare(const Sales_item &lhs, const Sales_item &rhs)
     {
         return lhs->book() < rhs->book();
     }

Our compare function has the same interface as the less-than operator. It returns a bool and takes two const references to Sales_items. It compares the parameters by comparing their ISBNs. This function uses the Sales_item -> operator, which returns a pointer to an Item_base object. That pointer is used to fetch and run the book member, which returns the ISBN.

我们的 compare 函数与小于操作符有两样的接口,它接受两个 Sales_item 对象的 const 引用,通过比较 ISBN 而比较形参,返回一个 book 值。该函数使用 Sales_item-> 操作符,该操作符返回 Item_base 对象的指针,那个指针用于获取并运行成员 book,该成员返回 ISBN。

Using a Comparator with an Associative Container
使用带关联容器的比较器

If we think a bit about how the comparison function is used, we'll realize that it must be stored as part of the container. The comparison function is used by any operation that adds or finds an element in the container. In principle, each of these operations could take an optional extra argument that represented the comparison function. However, this strategy would be error-prone: If two operations used different comparison functions, then the ordering would be inconsistent. It's impossible to predict what would happen in practice.

如果考虑一下如何使用比较函数,就会认识到,它必须作为容器的部分而存储。任何在容器中增加或查找元素的操作都要使用比较函数。原则上,每个这样的操作可以接受一个可选的附加实参,表示比较函数。但是,这种策略容易导致出错:如果两个操作使用不同的比较函数,顺序可能会不一致。不可能预测实际上会发生什么。

To work effectively, an associative container needs to use the same comparison function for every operation. Yet, it is unreasonable to expect users to remember the comparison function every time, especially when there is no way to check that each call uses the same comparison function. Therefore, it makes sense for the container to remember the comparison function. By storing the comparator in the container object, we are assured that every operation that compares elements will do so consistently.

要有效地工作,关联容器需要对每个操作使用同一比较函数。然而,期望用户每次记住比较函数是不合理的,尤其是,没有办法检查每个调用使用同一比较函数。因此,容器记住比较函数是有意义的。通过将比较器存储在容器对象中,可以保证比较元素的每个操作将一致地进行。

For the same reasons that the container needs to know the element type, it needs to know the comparator type in order to store the comparator. In principle, the container could infer this type by assuming that the comparator is pointer to a function that returns a bool and takes references to two objects of the key_type of the container. Unfortunately, this inferred type would be overly restrictive. For one thing, we should allow the comparator to be a function object as well as a plain function. Even if we were willing to require that the comparator be a function, the inferred type would still be too restrictive. After all, the comparison function might return an int or any other type that can be used in a condition. Similarly, the parameter type need not exactly match the key_type. Any parameter type that is convertible to the key_type should also be allowed.

基于同样的理由,容器需要知道元素类型,为了存储比较器,它需要知道比较器类型。原则上,通过假定比较器是一个函数指针,该函数接受两个容器的 key_type 类型的对象并返回 bool 值,容器可以推断出这个类型。不幸的是,这个推断出的类型可能限制太大。首先,应该允许比较器是函数对象或是普通函数。即使我们愿意要求比较器为函数,这个推断出的类型也可能仍然太受限制了,毕竟,比较函数可以返回 int 或者其他任意可用在条件中的类型。同样,形参类型也不需要与 key_type 完全匹配,应该允许可以转换为 key_type 的任意形参类型。

So, to use our Sales_item comparison function, we must specify the comparator type when we define the multiset. In our case, that type is a function that returns a bool and takes two const Sales_item references.

所以,要使用 Sales_item 的比较函数,在定义 multiset 时必须指定比较器类型。在我们的例子中,比较器类型是接受两个 const Sales_item 引用并返回 bool 值的函数。

We'll start by defining a typedef that is a synonym for this type (Section 7.9, p. 276):

首先定义一个类型别名,作为该类型的同义词(第 7.9 节):

     // type of the comparison function used to order the multiset
     typedef bool (*Comp)(const Sales_item&, const Sales_item&);

This statement defines Comp as a synonym for the pointer to function type that matches the comparison function we wish to use to compare Sales_item objects.

这个语句将 Comp 定义为函数类型指针的同义词,该函数类型与我们希望用来比较 Sales_item 对象的比较函数相匹配。

Next we'll need to define a multiset that holds objects of type Sales_item and that uses this Comp type for its comparison function. Each constructor for the associative containers allows us to supply the name of the comparison function. We can define an empty multiset that uses our compare function as follows:

接着需要定义 multiset,保存 Sales_item 类型的对象并在它的比较函数中使用这个 Comp 类型。关联容器的每个构造函数使我们能够提供比较函数的名字。可以这样定义使用 compare 函数的空 multiset

     std::multiset<Sales_item, Comp> items(compare);

This definition says that items is a multiset that holds Sales_item objects and uses an object of type Comp to compare them. The multiset is emptywe supplied no elementsbut we did supply a comparison function named compare. When we add or look for elements in items our compare function will be used to order the multiset.

这个定义是说,items 是一个 multiset,它保存 Sales_item 对象并使用 Comp 类型的对象比较它们。multiset 是空的——我们没有提供任何元素,但我们的确提供了一个名为 compare 的比较函数。当在 items 中增加或查找元素时,将用 compare 函数对 multiset 进行排序。

Containers and Handle Classes
容器与句柄类

Now that we know how to supply a comparison function, we'll define a class, named Basket, to keep track of a sale and calculate the purchase price:

既然知道了怎样提供比较函数,我们将定义名为 Basker 的类,以跟踪销售并计算购买价格:

     class Basket {
         // type of the comparison function used to order the multiset
         typedef bool (*Comp)(const Sales_item&, const Sales_item&);
     public:
         // make it easier to type the type of our set
         typedef std::multiset<Sales_item, Comp> set_type;
         // typedefs modeled after corresponding container types
         typedef set_type::size_type size_type;
         typedef set_type::const_iterator const_iter;
         Basket(): items(compare) { } // initialze the comparator
         void add_item(const Sales_item &item)
                             { items.insert(item); }
         size_type size(const Sales_item &i) const
                              { return items.count(i); }
         double total() const; // sum of net prices for all items in the basket
     private:
         std::multiset<Sales_item, Comp> items;
     };

This class holds the customer's purchases in a multiset of Sales_item objects. We use a multiset to allow the customer to buy multiple copies of the same book.

这个类在 Sales_item 对象的 multiple 中保存顾客购买的商品,用 multiple 使顾客能够购买同一本书的多个副本。

The class defines a single constructor, the Basket default constructor. The class needs its own default constructor to pass compare to the multiset constructor that builds the items member.

该类定义了一个构造函数,即 Basket 默认构造函数。该类需要自己的默认构造函数,以便将 compare 传给建立 items 成员的 multiset 构造函数。

The operations that the Basket class defines are fairly simple: add_item takes a reference to a Sales_item and puts a copy of that item into the multiset; item_count returns the number of records for this ISBN in the basket for a given ISBN. In addition to the operations, Basket defines three typedefs to make it easier to use its multiset member.

Basket 类定义的操作非常简单:add_item 操作接受 Sales_item 对象引用并将该项目的副本放入 multiset;对于给定 ISBN,size 操作返回购物篮中该 ISBN 的记录数。除了操作,Basket 还定义了三个类型别名,这样使用它的 multiset 成员就比较容易了。

Using the Handle to Execute a Virtual Function
使用句柄执行虚函数

The only complicated member of class Basket is the total function, which returns the price for all the items in the basket:

Basket 类唯一的复杂成员是 total 函数,该函数返回购物篮中所有物品的价格:

     double Basket::total() const
     {
         double sum = 0.0; // holds the running total

         /* find each set of items with the same isbn and calculate
          * the net price for that quantity of items
          * iter refers to first copy of each book in the set
          * upper_bound refers to next element with a different isbn
          */
          for (const_iter iter = items.begin();
                                 iter != items.end(); iter =
                                 items.upper_bound(*iter))
     {
              // we know there's at least one element with this key in the Basket
              // virtual call to net_price applies appropriate discounts, if any
              sum += (*iter)->net_price(items.count(*iter));
          }
          return sum;
     }

The total function has two interesting parts: the call to the net_price function, and the structure of the for loop. We'll look at each in turn.

total 函数有两个有趣的部分:对 net_price 函数的调用,以及 for 循环结构。我们逐一进行分析。

When we call net_price, we need to tell it how many copies of a given book are being purchased. The net_price function uses this argument to determine whether the purchase qualifies for a discount. This requirement implies that we'd like to process the multiset in chunksprocessing all the records for a given title in one chunk and then the set of those for the next title and so on. Fortunately, multiset is well suited to this problem.

调用 net_price 函数时,需要告诉它某本书已经购买了多少本,net_price 函数使用这个实参确定是否打折。这个要求暗示着我们希望成批处理 multiset——处理给定标题的所有记录,然后处理下一个标题的所有记录,以此类推。幸好,multiset 非常适合处理这个问题。

Our for loop starts by defining and initializing iter to refer to the first element in the multiset. We use the multiset count member (Section 10.3.6, p. 367) to determine how many elements in the multiset have the same key (e.g., same isbn) and use that number as the argument to the call to net_price.

for 循环开始于定义 iter 并将 iter 初始化为指向 multiset 中的第一个元素。我们使用 multisetcount 成员(第 10.3.6 节)确定 multiset 中的多少成员具有相同的键(即,相同的 isbn),并且使用该数目作为实参调用 net_price 函数。

The interesting bit is the "increment" expression in the for. Rather than the usual loop that reads each element, we advance iter to refer to the next key. We skip over all the elements that match the current key by calling upper_bound (Section 10.5.2, p. 377). The call to upper_bound returns the iterator that refers to the element just past the last one with the same key as in iter. That iterator we get back denotes either the end of the set or the next unique book. We test the new value of iter. If iter is equal to items.end(), we drop out of the for. Otherwise, we process the next book.

for 循环中的“增量”表达式很有意思。与读每个元素的一般循环不同,我们推进 iter 指向下一个键。调用 upper_bound 函数以跳过与当前键匹配的所有元素,upper_bound 函数的调用返回一个迭代器,该迭代器指向与 iter 键相同的最后一个元素的下一元素,即,该迭代器指向集合的末尾或下一本书。测试 iter 的新值,如果与 items.end() 相等,则跳出 for 循环,否则,就处理下一本书。

The body of the for calls the net_price function. That call can be a bit tricky to read:

for 循环的循环体调用 net_price 函数,阅读这个调用需要一点技巧:

     sum += (*iter)->net_price(items.count(*iter));

We dereference iter to get the underlying Sales_item to which we apply the overloaded arrow operator from the Sales_item class. That operator returns the underlying Item_base object to which the handle is attached. From that object we call net_price, passing the count of items with the same isbn. The net_price function is virtual, so the version of the pricing function that is called depends on the type of the underlying Item_base object.

iter 解引用获得基础 Sales_item 对象,对该对象应用 Sales_item 类重载的箭头操作符,该操作符返回句柄所关联的基础 Item_base 对象,用该 Item_base 对象调用 net_price 函数,传递具有相同 isbn 的图书的 count 作为实参。net_price 是虚函数,所以调用的定价函数的版本取决于基础 Item_base 对象的类型。

Exercises Section 15.8.3

Exercise 15.35:

Write your own version of the compare function and Basket class and use them to manage a sale.

编写自己的 compare 函数和 Basket 类的版本并使用它们管理销售。

Exercise 15.36:

What is the underlying type of Basket::const_iter?

Basket::const_iter 的基础类型是什么?

Exercise 15.37:

Why did we define the Comp typedef in the private part of Basket?

为什么在 Basketprivate 部分定义 Comp 类型别名?

Exercise 15.38:

Why did we define two private sections in Basket?

为什么在 Basket 中定义两个 private 部分?


Team LiB
Previous Section Next Section