Journal Articles
Browse in : |
All
> Journals
> Overload
> 27
(10)
All > Topics > Programming (877) Any of these categories - All of these categories |
Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.
Title: Object (low-level) Design and Implementation
Author: Administrator
Date: 27 October 1999 18:22:42 +01:00 or Wed, 27 October 1999 18:22:42 +01:00
Summary:
Body:
I was delighted to receive not just one but two responses to my last article, see the preceding articles. Before I go any further let me respond to the substance of these items.
The introduction of exceptions into C++ raises a number of design issues and it has taken several years for the best C++ programmers to refine their understanding of their correct use. The concept of an exception specification caused considerable trouble. It is my understanding that the UK originally wanted them removed because they could not be used for static checking of code. That left the problem that they required a runtime feature to support them.
While in the strictest terms this is correct, exception specifications provide a number of positive benefits. While complete static checking cannot be provided, some static checking is possible. For example:
void fn() throw() { Mytype * ptr = new Mytype; // rest of function }
Can be checked. It does not catch the bad_alloc exception that new can produce and so is clearly making a promise that cannot be kept. Any halfway reasonable compiler should raise an objection to such code.
The second thing, that exception specifiers provide, is a statement of intent for the benefit of other programmers. It is a condition applied to the function, and like all other features of a declaration it provides a constraint that users can (or should be able to) rely on. Once I decide, as part of my low-level design, that a function does not allow exceptions to leak it is a commitment that I must abide by. Like the return type, a throw specifier is part of the signature of a function that cannot be overloaded.
Readers of the latest edition of 'The C++ Programming Language' will know that there are some clever fixes that can be applied to handle functions that are not supposed to throw exceptions by providing special versions of the handler for 'unexpected', but I will leave that to experts.
Whether read functions should or should not have an empty exception specifier is a class design decision, however the logic of Detlef's letter would be that we should never use exception specifiers because they commit us for all time to a specific policy with regards to a function. I find this too negative. So, let me explore some options.
The first is to provide overloading via an extra dummy parameter. For example, suppose we declare a global enum type:
enum CanThrow {canThrow};
Now suitable pairs of functions can co-exist:
string const & getName() const throw(); string const & getName(CanThrow) const;
This empowers the user of the Customer class to write either:
cout << customer.getName();
or
cout << customer.getName(canThrow);
depending on whether the user wants to handle exceptions or not. That means that the version with an empty exception specifier must handle exceptions internally and provide some dummy return if no genuine value is available. The definition of the 'throwing' version should use the anonymous parameter facility to handle the CanThrow parameter because there is no practical use of the parameter in the body of the function. The parameter is purely to provide overloading, and the type name and value are chosen to alert the user to the need to handle exceptions.
The second option is to have a specific exception type as the only one that can be thrown by the function. Something along the lines of:
enum NoName{noName}; string const & getName() const throw(NoName);
and the definition would be:
string const & getName() const throw(NoName) { try { // body of function } catch (…) { throw noName; } }
Of course there could be many more catch clauses that handled individual problems but each would terminate with either a return of some string or with throw(noName).
Programmers should know which exceptions they may have to handle. Until library designers get in the habit of providing exception specifiers, programmers must assume that all exceptions may need handling. We can argue about the merits of different strategies, but pretending that we need do nothing isn't a professional option. Like too many things however, exception specifiers are going to be ignored by most authors of books because they will seem like just another complication.
While I agree with Detlef that it is a (low-level) design decision as to what exception specifier should be attached to ordinary member functions I completely disagree when it comes to destructors. I think he has misunderstood the purpose of uncaught_ exception(). But I will return to that in a moment.
Constructors can and should be able to throw exceptions. If something goes wrong during the process of constructing an object some way is needed to get your program back onto safe ground. The biggest problem was finding a mechanism to handle an exception thrown from some part of a constructor-initialiser list. I believe that this problem actually generated some new syntax so that entire function definitions could be encapsulated in a try block but I have never seen this used. Suffice to say that the exception mechanism is particularly useful for dealing with problems during the process of construction.
But what about the other end of an object's life? Suppose that a destructor throws an exception, what am I supposed to do? In general all I will know is that I am handling an exception, no clue that I have an incompletely destroyed object on my hands. For example, suppose that I have some local object that handles a file and a serial port. The destructor is called for the object when the function is cleaning up before returning. Something happens during the process of closing the file that results in an exception, unless that exception is handled locally the serial port is never released. OK that is a bit obvious and the programmer of the destructor should handle that but what if he doesn't? Your program is now unstable and should raise an 'unexpected' exception.
I am not going to claim that no destructor should ever, under any circumstances, throw an exception. What I do claim is that if a designer finds it necessary to allow a destructor to throw then the exact nature of the exception must be documented and a full justification for allowing it should be required.
In my opinion, destructors should always have exception specifiers. In the overwhelming majority such a specifier should be empty. I believe that writing a destructor without an exception specifier is unprofessional and a sign of incompetence or ignorance. Yes we all forget sometimes, but we should be embarrassed if it happens very often.
Now a brief word about 'uncaught_exception'. When we write functions that may be used inside exception handlers we have to consider that possibility and arrange some tolerable behaviour (if possible) when they would normally throw an exception. The purpose of uncaught_exception() is to provide the tool that programmers can use when they have no other viable alternative. This is particularly true of mission critical programs that must not abort. Every effort should be made to ensure that functions used during the process of handling an exception do not throw exceptions. Only in the most unusual circumstances might you tolerate different behaviour from a function depending upon whether it was called during exception handling or otherwise. Such special behaviour during EH would be some compromise (such as letting a resource leak) that was undesirable but less so than aborting the process.
I would welcome alternative views on this subject because I currently can see no justification for allowing a destructor licence to throw anything and everything.
I am very grateful for Roger Lever's thoughtful commentary on the subject of design. I think one problem is that the term is used in several ways. I think that I ma largely focused on the low-level aspects. To me, design is a matter of deciding what a class (or function) shall do while implementation is a matter of deciding how it shall do it. I consider design to be a matter of deciding what the interfaces of a class shall be. Roger, quite correctly, is taking the broader view that design is a matter of deciding what a class is for. Let me try to elucidate, and Roger can come back next time to correct me as appropriate.
What constitutes a 'room' depends upon whose viewpoint you take. An architect has one view, and architectural engineer (responsible for considering such things as the loading on floors, the stresses on walls etc.) has another. The architect might be concerned about the placement of windows, the shape of the room, the location of a fireplace etc. without too much regard as to other rooms adjacent to the one in focus. The architectural engineer has to consider what is adjacent. It is her job to note external walls and the potential for heat loss, the existence of upper floors with the consequential requirements for load bearing walls.
One of the UK TV channels has been running a series of programs on design. The second of these was about designing a new toilet for a leading UK manufacturer of bathroom suites. They had commissioned two designers. It became clear during the course of the program that the designers and the company directors meant very different things be a design and design brief. The company was mainly concerned with the external appearance and just wanted a new (but not too new) 'shape'. The designers wanted to consider the function and produce something that better met the needs of male and female users, was easier to keep clean etc. There was another aspect to this in that those actually responsible for production (the 'implementors') had another view - what could practically be produced by the equipment available. Moulds must work, the items must be fired without too much wastage etc.
Professional designers should provide design documents for their code. These should be based on an understanding as to what aspects of objects are to be represented. The hotel designer, the builder and the receptionist have very different views as to what is important about a room.
In the days before we focused strongly on reuse the context of the application we were writing implicitly defined the design (making it explicit would have been a good thing and much of the abuse of code by cut and paste coding might have been avoided had programmers had a better understanding of the relevance of viewpoint to code design). Now that we increasingly focus on reuse we need to be conscious of reuse at all levels.
My view (and I think that intended by Paul) is that of the manager of the hotel. Roger suggests that we should up this a level to that of the manager of a chain of hotels. I think this is an excellent extension and the kind of thing that comes with increased experience of using object-oriented techniques. However I am mainly focusing on low-level design because, no matter how elegant the high-level design, without good low-level design everything falls apart. Look at an ordinary building brick. There is a lot of low-level design involved. For example, the shape is a cuboid whose dimensions are approximately 3:2:1. The dimensions are intended to be exactly 3:2:1 when the thickness of the mortar is taken into account. If you did not know how bricks were used you might be puzzled by the approximations.
There seems to be a widely held belief that the default behaviour of providing copy constructors and copy assignment is correct. I reject this. Consider my favourite 'playing card' type. How many Spade Aces should there be in a pack of cards? One, and if you were playing Poker and two Spade Aces turned up you would know someone was cheating. Each card in a back is a unique item in context. It might be possible to duplicate that item but such duplication should be a careful and considered action, not some by-product of a desire to have a second object that was identical to the first. This is even more the case when it comes to assignment. It should be completely meaningless to assign one object to another.
I think that object types should never have public copy constructors and copy assignments. Sometimes it may be desirable to provide such functionality privately, or even to other class designers via the protected interface. The existence of a public copy constructor is what distinguishes a value type from and object type. Values may be freely copied, objects should only be cloned. If you do not understand this distinction you do not understand object based/oriented programming.
Unfortunately we get very casual about our use of terminology. We often talk about throwing an exception object. We should never do this. We should throw an exception value (remember that exception 'objects' are always copied to the point where they are caught). This looseness does not matter as long as we understand what we mean, sadly many of those listening do not and so get confused.
So let me consider my Customer type. Should this be a value or an object type? I think we must be careful about what we mean. There is nothing to prevent us from having multiple but identical objects. Indeed my junk mail shows that many companies are quite happy with having multiple instances of me in their databases. What I am asking is should we allow a 'Customer' to be copied without explicitly choosing to do so? My feeling is that the answer should be 'no'.
There is a problem with strictly adhering to the concept of an object and removing publicly available copy constructors: all the STL containers are value based. In other words the STL containers require access to copy constructors. We would expect to be able to produce a customer list, yet to do so we must provide access to a copy constructor. Before we consider possible solutions we must ask ourselves about our concept of a customer and how we expect it to be used. Is 'customer' intended to be a base class? In other words, do we expect to derive from customer? If so we cannot have a simple container of customers because the STL containers do not work well with polymorphic types (unless they all have the same size, which is unlikely). If we want to manage collections of polymorphic objects we must provide a surrogate or handle type, a smart pointer or use a raw pointer. A suitably designed smart pointer (not, PLEASE, auto_ptr, because that was not designed for such use) would be best because it would handle extensions to customer easily (the cost is in designing the smart pointer, anyone offer a smart pointer for container use?) might be best but a well designed surrogate would be good as well. I would not be keen on using raw pointers as they would be responsible for large scale resource leakage.
Our collections would have to manage our objects via (smart-)pointers or surrogates which might have public copy constructors. Actually, I am slightly uneasy with the concept of a surrogate with a public copy constructor.
On the other hand if you have an essentially non-polymorphic object type (playing cards would be a good example) then we can fix the problem in a different way. Let me give you an example:
class PlayingCard { friend vector<PlayingCard>; PlayingCard(PlayingCard const&); void operator =(PlayingCard const &); // rest of class interfaces };
By making vector<PlayingCard> a friend of PlayingCard I have provided it access to the private copy constructor. Of course the only containers you can have will be vectors, perhaps you might want to add:
friend list<PlayingCard>;
as well.
I think that this is a legitimate use for friend. What do you think? I wish that there was a way to provide special access to the protected interface so that I could grant special access rights to third parties without having to go the whole way and give them access to everything.
I am never very happy with this term and suspect that it is often misused. I understand that it originated from the idea of basic ice creams to which a selection of extras could be added. In programming terms it seems to refer to a basic class to which various extras can be added by multiple inheritance (Java, I guess, would use interfaces for this purpose). The idea is that these extras are free standing abstract base classes that represent some specific abstraction. In the context of our hotel as a commercial enterprise we have a couple of candidates for 'mixins'.
The concept of being hireable is one that applies to much more than rooms and presentation equipment. Complementary with the concept of being hireable is the concept of being billable.
Hireable might be provided by something along the lines of:
class Hireable { ChargeInfo * rates; public: Hireable(ChargeInfo *lookup = 0) : rates(lookup){} // despite the pointer, // shallow copies work Currency getRate(TimePeriod) throw(Invalid) const; void setRate(ChargeInfo *) throw (); virtual ~Hireable() throw() = 0; };
This class raises a number of issues. The first is that several other ADTs naturally arise and will have to be designed and implemented. Anything that is hireable will have to have some form of rate-table. We will also need some form of time information (hourly, daily, weekly etc.) and something to represent the currency used. I am not providing details of these but have added them to highlight the kind of thing that starts to happen as you try to work in an OO fashion. It would seem that ChargeInfo should be some kind of external data structure that can be accessed with TimePeriod data. I have used a pointer rather than a reference because it seems likely that you might want to replace the rate-table, you will also need to handle the creation of hireable objects even if you do not know what rate-table to use. The nature of ChargeInfo is left for consideration by the designer of that class with the proviso that it should work with the TimePeriod class to generate Currency information.
There is another interesting aspect of this class in that it is a user of ChargeInfo but is not responsible for its creation. That means that the raw pointer can be copied by both copy-constructor and copy assignment. It is not always the case that you must provide the copying functions if one or more data element is a pointer. On the other hand this is a risky technique because we are using a pointer to data that is outside the control of the object. Such pointers are always vulnerable to becoming hanging pointers if the object they are pointing to is removed or relocated.
The reason for taking this risk is that many objects may need to share a look-up table of rates. Such a table would be subject to amendment and so needs to be unique. There is another option. We can allow each object to hold its own local copy and register this information with the master copy. The functions that change the master copy would then be responsible for notifying the copy-holders. The destructor for the master would then be responsible for notifying all current holders of local copies to reset their pointers either to null or to some substitute. In the long term a technique such as this is preferable and professional class designers should be familiar with the idea and the principles for implementing it. Experience suggests that few are.
The reason that an empty destructor has been declared is to provide a hook for making Hireable an abstract base class. Hireable is an abstraction and we do not want free standing instances. What we want to be able to produce is something like:
class RentableRoom : public Room, public Hireable { // what ever };
As we think deeper and deeper into this problem we become aware of many other classes that we should work on. For example we will need to consider the payment method (cash, credit card, cheque etc.) This seems a good target for a class hierarchy with an abstract base class PaymentMethod and shallow hierarchy to provide the various options.
I think it is because of this requirement to add layers of classes that so many programmers retreat to simple non-reusable solutions.
This article is already late and getting rather long. I think it is about time that I looked at some more of Paul's code. This time I am going to look at some of his implementation.
#include <iostream.h> #include <string.h> const int maxName = 30; // reserve storage for the static int Customer::customerCount; Customer::Customer( { char temp[maxName]; int size; cout << "Enter customer name: "; cin >> temp; size = strlen(temp); name = new char[size + 1]; strcpy(name, temp); cout << "Enter payee name"; cin >> temp; size = strlen(temp); payee = new char[size + 1]; strcpy(payee, temp); customerCount++; }
When we look at the above code we will realise that several poor decisions relate back to his original design. The pollution of the global namespace by maxName (an unfortunate choice of identifier as it is certain to be popular in other code written at the same level of expertise - a good reason for hiding such in a named namespace) can be avoided by recognising that the only code that depends upon this value is the temporary array of char used to capture the data. In such a case the manifest constant should be declared close to its point of use (like immediately before its first use). Of course once we feel comfortable with using string instead of char[] the problem goes away, though might still want to apply some form of validation by restricting the number of characters used. I always feel unhappy with the use of int for the sizes of things. Surely this should either be size_t or unsigned int?
The next problem is that no attempt has been made to ensure that the input does not over-write the provided storage, nor has code been provided to handle names that include embedded whitespace.
I offer the following re-write (I am focusing on implementation here because writing good implementation code is also important).
// select standard library identifiers using std::cin; using std::cout; using std::istream::get; // reserve storage for the static int Customer::customerCount = 0; Customer::Customer() { const int maxNameLength = 30; char temp[maxNameLength]; size_t size; cout << "Enter customer name: "; cin.get(temp, maxNameLength); size = strlen(temp); // clear input buffer while(cin.get()!= '\n'); name = new char[size + 1]; strcpy(name, temp); cout << "Enter payee name"; cin.get(temp, maxNameLength); size = strlen(temp); // clear input buffer while(cin.get()!= '\n'); name = new char[size + 1]; strcpy(payee, temp); customerCount++; }
As I wrote this I became very conscious that a large chunk of that code is almost duplicated. That provides a maintenance problem as well as making the function larger than necessary (pragmatically the error rate goes up as function size increases). Consider the following alternative:
void initName(char const * prompt, char * & dest) { const int maxNameLength = 30; char temp[maxNameLength]; size_t size; cout << prompt; cin.get(temp, maxNameLength); size = strlen(temp); // clear input buffer while(cin.get()!= '\n'); dest = new char[size + 1]; strcpy(dest, temp); } Customer::Customer() { initName("Customer name: ", name); initName("Payee name)", payee); customerCount++; }
Note the type of the second parameter of initName(). This handles a situation that many programmers get wrong. I want to pass a pointer for modification. By passing a reference to a pointer I make it more likely that I will write what I intend. By the way should initName be a private member function? Perhaps it should be a utility function in namespace Harpist, or perhaps there should be a third parameter passing the maximum acceptable length. As I would use string instead of char[] I am not going to worry too much this time around, but this kind of small utility function is a prime candidate for very low-level reuse.
Customer::~Customer() { customerCount--; delete name; delete payee; } int Customer::getCustomerCount() { return customerCount; } char* Customer::getName() { return name; } char* Customer::getPayee() { return payee; }
Of course these last two functions are badly flawed because they provide write access to private data and hence allows fraudulent changes to the data. We know that the original design was faulty because it failed to qualify these functions as const (read only) and without that qualification we can get the return type wrong. With the qualification the compiler knows that we have just provided illegal write access to instance data. The various uses of const are designed to reinforce each other. However I am just improving the implementation of the original so these two functions should at least become:
char const * Customer::getName() { return name; } char const * Customer::getPayee() { return payee; }
Well I think that is all I have time for this time. Keep the comments flowing so that we all become better C++ programmers.
Notes:
More fields may be available via dynamicdata ..