📄 mc5.htm
字号:
static int numObjects; static const size_t maxObjects; void init(); // to avoid ctor code}; // duplicationtemplate<class BeingCounted>Counted<BeingCounted>::Counted(){ init(); }template<class BeingCounted>Counted<BeingCounted>::Counted(const Counted<BeingCounted>&){ init(); }template<class BeingCounted>void Counted<BeingCounted>::init(){ if (numObjects >= maxObjects) throw TooManyObjects(); ++numObjects;}The classes generated from this template are designed to be used only as base classes, hence the protected constructors and destructor. Note the use of the private member function init to avoid duplicating the statements in the two Counted constructors.We can now modify the Printer class to use the Counted template: class Printer: private Counted<Printer> {public: // pseudo-constructors static Printer * makePrinter(); static Printer * makePrinter(const Printer& rhs); ~Printer(); void submitJob(const PrintJob& job); void reset(); void performSelfTest(); ... using Counted<Printer>::objectCount; // see below using Counted<Printer>::TooManyObjects; // see belowprivate: Printer(); Printer(const Printer& rhs);};The fact that Printer uses the Counted template to keep track of how many Printer objects exist is, frankly, nobody's business but the author of Printer's. Such implementation details are best kept private, and that's why private inheritance is used here (see Item E42). The alternative would be to use public inheritance between Printer and Counted<Printer>, but then we'd be obliged to give the Counted classes a virtual destructor. (Otherwise we'd risk incorrect behavior if somebody deleted a Printer object through a Counted<Printer>* pointer see Item E14.) As Item 24 makes clear, the presence of a virtual function in Counted would almost certainly affect the size and layout of objects of classes inheriting from Counted. We don't want to absorb that overhead, and the use of private inheritance lets us avoid it.Quite properly, most of what Counted does is hidden from Printer's clients, but those clients might reasonably want to find out how many Printer objects exist. The Counted template offers the objectCount function to provide this information, but that function becomes private in Printer due to our use of private inheritance. To restore the public accessibility of that function, we employ a using declaration: class Printer: private Counted<Printer> {public: ... using Counted<Printer>::objectCount; // make this function // public for clients ... // of Printer};This is perfectly legitimate, but if your compilers don't yet support namespaces, they won't allow it. If they don't, you can use the older access declaration syntax: class Printer: private Counted<Printer> {public: ... Counted<Printer>::objectCount; // make objectCount // public in Printer ...};This more traditional syntax has the same meaning as the using declaration, but it's deprecated. The class TooManyObjects is handled in the same fashion as objectCount, because clients of Printer must have access to TooManyObjects if they are to be able to catch exceptions of that type.When Printer inherits from Counted<Printer>, it can forget about counting objects. The class can be written as if somebody else were doing the counting for it, because somebody else (Counted<Printer>) is. A Printer constructor now looks like this: Printer::Printer(){ proceed with normal object construction;}What's interesting here is not what you see, it's what you don't. No checking of the number of objects to see if the limit is about to be exceeded, no incrementing the number of objects in existence once the constructor is done. All that is now handled by the Counted<Printer> constructors, and because Counted<Printer> is a base class of Printer, we know that a Counted<Printer> constructor will always be called before a Printer constructor. If too many objects are created, a Counted<Printer> constructor throws an exception, and the Printer constructor won't even be invoked. Nifty, huh?Nifty or not, there's one loose end that demands to be tied, and that's the mandatory definitions of the statics inside Counted. It's easy enough to take care of numObjects we just put this in Counted's implementation file: template<class BeingCounted> // defines numObjectsint Counted<BeingCounted>::numObjects; // and automatically // initializes it to 0The situation with maxObjects is a bit trickier. To what value should we initialize this variable? If we want to allow up to 10 printers, we should initialize Counted<Printer>::maxObjects to 10. If, on the other hand, we want to allow up to 16 file descriptor objects, we should initialize Counted<FileDescriptor>::maxObjects to 16. What to do?We take the easy way out: we do nothing. We provide no initialization at all for maxObjects. Instead, we require that clients of the class provide the appropriate initialization. The author of Printer must add this to an implementation file: const size_t Counted<Printer>::maxObjects = 10;Similarly, the author of FileDescriptor must add this: const size_t Counted<FileDescriptor>::maxObjects = 16;What will happen if these authors forget to provide a suitable definition for maxObjects? Simple: they'll get an error during linking, because maxObjects will be undefined. Provided we've adequately documented this requirement for clients of Counted, they can then say "Duh" to themselves and go back and add the requisite initialization. Back to Item 26: Limiting the number of objects of a classContinue to Item 28: Smart pointersItem 27: Requiring or prohibiting heap-based objects.Sometimes you want to arrange things so that objects of a particular type can commit suicide, i.e., can "delete this." Such an arrangement clearly requires that objects of that type be allocated on the heap. Other times you'll want to bask in the certainty that there can be no memory leaks for a particular class, because none of the objects could have been allocated on the heap. This might be the case if you are working on an embedded system, where memory leaks are especially troublesome and heap space is at a premium. Is it possible to produce code that requires or prohibits heap-based objects? Often it is, but it also turns out that the notion of being "on the heap" is more nebulous than you might think.Requiring Heap-Based ObjectsLet us begin with the prospect of limiting object creation to the heap. To enforce such a restriction, you've got to find a way to prevent clients from creating objects other than by calling new. This is easy to do. Non-heap objects are automatically constructed at their point of definition and automatically destructed at the end of their lifetime, so it suffices to simply make these implicit constructions and destructions illegal.The straightforward way to make these calls illegal is to declare the constructors and the destructor private. This is overkill. There's no reason why they both need to be private. Better to make the destructor private and the constructors public. Then, in a process that should be familiar from Item 26, you can introduce a privileged pseudo-destructor function that has access to the real destructor. Clients then call the pseudo-destructor to destroy the objects they've created.If, for example, we want to ensure that objects representing unlimited precision numbers are created only on the heap, we can do it like this: class UPNumber {public: UPNumber(); UPNumber(int initValue); UPNumber(double initValue); UPNumber(const UPNumber& rhs); // pseudo-destructor (a const member function, because // even const objects may be destroyed) void destroy() const { delete this; } ...private: ~UPNumber();};Clients would then program like this: UPNumber n; // error! (legal here, but // illegal when n's dtor is // later implicitly invoked)UPNumber *p = new UPNumber; // fine...delete p; // error! attempt to call // private destructorp->destroy(); // fineAn alternative is to declare all the constructors private. The drawback to that idea is that a class often has many constructors, and the class's author must remember to declare each of them private. This includes the copy constructor, and it may include a default constructor, too, if these functions would otherwise be generated by compilers; compiler-generated functions are always public (see Item E45). As a result, it's easier to declare only the destructor private, because a class can have only one of those.Restricting access to a class's destructor or its constructors prevents the creation of non-heap objects, but, in a story that is told in Item 26, it also prevents both inheritance and containment: class UPNumber { ... }; // declares dtor or ctors // privateclass NonNegativeUPNumber: public UPNumber { ... }; // error! dtor or ctors // won't compileclass Asset {private: UPNumber value; ... // error! dtor or ctors // won't compile};Neither of these difficulties is insurmountable. The inheritance problem can be solved by making UPNumber's destructor protected (while keeping its constructors public), and classes that need to contain objects of type UPNumber can be modified to contain pointers to UPNumber objects instead: class UPNumber { ... }; // declares dtor protectedclass NonNegativeUPNumber: public UPNumber { ... }; // now okay; derived // classes have access to // protected membersclass Asset {public: Asset(int initValue); ~Asset(); ...private: UPNumber *value;};Asset::Asset(int initValue): value(new UPNumber(initValue)) // fine{ ... }Asset::~Asset(){ value->destroy(); } // also fineDetermining Whether an Object is On The HeapIf we adopt this strategy, we must reexamine what it means to be "on the heap." Given the class definition sketched above, it's legal to define a non-heap NonNegativeUPNumber object: NonNegativeUPNumber n; // fineNow, the UPNumber part of the NonNegativeUPNumber object n is not on the heap. Is that okay? The answer depends on the details of the class's design and implementation, but let us suppose it is not okay, that all UPNumber objects even base class parts of more derived objects must be on the heap. How can we enforce this restriction?There is no easy way. It is not possible for a UPNumber constructor to determine whether it's being invoked as the base class part of a heap-based object. That is, there is no way for the UPNumber constructor to detect that the following contexts are different: NonNegativeUPNumber *n1 = new NonNegativeUPNumber; // on heapNonNegativeUPNumber n2; // not on heapBut perhaps you don't believe me. Perhaps you think you can play games with the interaction among the new operator, operator new and the constructor that the new operator calls (see Item 8). Perhaps you think you can outsmart them all by modifying UPNumber as follows: class UPNumber {public: // exception to throw if a non-heap object is created class HeapConstraintViolation {}; static void * operator new(size_t size); UPNumber(); ...private: static bool onTheHeap; // inside ctors, whether // the object being ... // constructed is on heap};// obligatory definition of class staticbool UPNumber::onTheHeap = false;void *UPNumber::operator new(size_t size){ onTheHeap = true; return ::operator new(size);}
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -