Having studied the mechanisms, the next step is to find proper ways,
recipes as it were, to use these mechanisms.
What we need, in the terminology of
In this section, we will look at two basic idioms, idioms that every C++ programmer needs to master or at least understand. These idioms concern the definition of concrete data types or representation types and their efficient implementation. It is not immediately obvious what lessons can be drawn for the realization of abstract data types in other languages.
To verify whether a concrete data type meets the requirements imposed by the specification of the abstract data type is quite straightforward, although not always easy. However, the task of verifying whether a concrete data type is optimally implemented is rather less well defined. To arrive at an optimal implementation may involve a lot of skill and ingenuity, and in general it is hard to decide whether the right choices have been made. Establishing trade-offs and making choices, for better or worse, is a matter of experience, and crucially depends upon the skill in handling the tools and mechanisms available.
Following
As may be expected, there is a constructor for creating a string from a pointer to char, which is the low-level C representation of a string. The result of evaluating this constructor is to store the argument string in the private data member. The definition of a default constructor is mandatory. The default constructor of the string class is easily obtained by employing a default argument for the constructor. When no argument is provided the private (low-level) string pointer is set to the empty string. A default constructor is required, since, for instance when creating an array of strings, the user is not allowed to initialize the individual (string) objects created. In such cases, the compiler uses a default constructor, which may be (re)defined by the implementor of the class. The other mandatory constructor is a so-called copy constructor. This constructor is used when creating a string by copying another string object. Copying occurs for example when passing an object by value to a function or when returning an object by value as the result of a function. By default, the compiler defines a standard copy constructor, which makes a shallow copy of the object, that is a copy of only the data members of the object, not what they refer to if they are pointers. However, a shallow copy is not in all cases satisfactory. For instance, in our string example, a shallow copy may cause the object to refer to the same string. When deleting the objects, the shared string pointer will be deleted twice, which on some systems may lead to a core dump. The copy constructor, as defined in the example, takes care of creating an actual copy of the string. Similar considerations apply to the use of the assignment operator and hence this operator needs to be redefined as well.
canonical
class string { public: string(char* s="") { init(s); } string(string& a) { init((char*)a); } ~string() { delete p; } string& operator=( string& a ) { init((char*)a); return *this; } string operator+( string& a ); int length() { return strlen(p); } operator char*() { return p; } private: void init(char* s) { p = new char[strlen(s)+1]; strcpy(p,s); } char* p; };
An example of the use of the string class is given below
string s1("hello"), s2("world"); cout << (char*) s1 << (char*) s2 ; string s3(s1); string s4 = s2; cout << (char*) s3 << (char*) s4; string s5; s5 = s3 + " " + s4; cout << (char*) s5;The example shows the creation of two strings from pointers. These strings are written to standard output by using an explicit cast. Alternatively, the output operator might have been overloaded for string. Next, two strings are created as a copy from the previously created strings. Note that in both cases the copy constructor is called. The assignment operator is only used to store the result of concatenating the strings just created.
Evidently, the implementation of the string class is far from optimal. Both in performance and storage there is a lot of unnecessary overhead. In the next section, we will look at how to improve the actual behavior of string objects.
envelope
class string { public: string(char* s = "") { rep = new stringrep(s); } string(string& a) { rep = a.rep; rep->count++; } string& operator=( string& a ) { a.rep->count++; if (--rep->count <= 0 ) delete rep; rep = a.rep; return *this; } string operator+( string& a ); int length() { return strlen(rep->rep); } operator char*() { return rep->rep; } private: stringrep* rep; };
The idea of the envelope/letter idiom is that the program manipulates objects (letters) through special wrappers (envelopes) which contain a pointer to the associated letter. The envelope can deal with some general issues (for example, ensuring that store is managed correctly on assignment), while deferring other operations to the letter. This separation of concerns makes developing a suitable class interface easier. We will also refer to the envelope/letter idiom as the handler/body idiom. A string handler (envelope) class may be defined as in slide 2-handler.
letter
class stringrep { friend string; private: stringrep(char* s) { rep = new char[strlen(s)+1]; strcpy(rep,s); count = 1; } ~stringrep() { delete[] rep; } char* rep; int count; };
The class string is declared to be a friend of the stringrep class, to allow the string direct access to the data members. Notice that the class stringrep has no public constructors. It is not intended to be used by others. Only friend classes are allowed to create actual instances of it.
In later versions of C++ it is possible to nest class definitions. This may be convenient for keeping the class name space from being polluted by auxiliary classes. Evidently, our stringrep class may be defined within the scope of the class string. This is left as an exercise for the reader.