Main

March 08, 2004 (Monday)

Chapter 11: Addendum

Note the following sequence of invocations of constructors, destructors and assignment operators. In many cases the methods are invoked implicitly.

vector<string>
split(const string s)		//  (3) string::string(const string &)
{
	vector<string> ret;	//  (4) vector<string>::vector()
	// ...
	return ret;		//  (5) vector<string>::vector(const vector<string>&)
				//  (6) vector<string>::~vector()
}				//  (7) string::~string()

int main()
{
	vector<string>	v; 	//  (1) vector<string>::vector()
	string	line;		//  (2) string::string()
	// ...
	v = split(line);	//  (8) vector<string>::operator=(const vector<string> &)
				//  (9) vector<string>::~vector()

				// (10) string::~string() 
				// (11) vector::~vector()
}

In Steps 1 and 2, we call the default constructor for the v vector and line string. We then pass line as a value parameter to the split() function which causes string's copy constructor to be invoked (Step 3). When inside the split() function, we call the default constructor for the vector in order to build the ret local variable (Step 4).

When the split() has completed, it performs a return by value. At Step 5 we use the vector's copy constructor to create a temporary to hold the return value from the split() function. When the function ends we must destruct all values that were local to this function. This includes the ret local variable (Step 6) as well as the s value parameter (Step 7).

When the function returns, the temporary is assigned to the v variable using the vector's assignment operator (Step 8) and the temporary generated earlier is then destroyed (Step 9). After the program ends, we destroy the local string line (Step 10) and finally the vector v.

Chapter 12: Making class objects act like values

The following code uses the Vec class, which was implemented in Chapter 11, in order to implement a string-like class called Str. At the core of the implementation of the Str class is a private data member which is defined as a vector of characters (i.e. Vec<char>). Note that unlike the Vec class, the Str class is not a template class. Our Str class will maintain a collection of char types only.

class Str {
public:
	Str() { }
	Str(size_type n, char c): data(n, c) { }
	Str(const char* cp) {
		std::copy(cp, cp + std::strlen(cp), std::back_inserter(data));
	}
	template <class In> Str(In i, In j) {
		std::copy(i, j, std::back_inserter(data));
	}

private:
	Vec<char> data;
};

Str Constructors

The above class definition also defines four different constructors:

Note the absence of a copy constructor and destructor (and assignment operator, for that matter). Because the Str classes delegates all resource management to its nested Vec<char> object, the synthesized versions of the copy constructor, destructor and assignment operator are appropriate for the Str class. If, however, the Str class was storing, for example, a Vec of char * and was dynamically assigning memory to elements of this vector by using new and deletes, then the Str class would have to implement appropriate versions of the copy constructor, destructor and assignment operator.

Types and Iterators, Size and Index Methods

As with the Vec class that we discussed last lecture, we add (nested) types representing size_type, iterator and const_iterator. We also add an appropriate size() method and const and non-const versions of the begin() and end() iterator methods. const and non-const methods that overload the [] operator are also added. All these functions delegate their tasks to the data member and are essentially identical to the corresponding versions in the Vec class.

class Str {
public:
	typedef Vec<char>::size_type size_type;
	typedef char* iterator;
	typedef const char* const_iterator;

	Str() { }
	Str(size_type n, char c): data(n, c) { }
	Str(const char* cp) {
		std::copy(cp, cp + std::strlen(cp), std::back_inserter(data));
	}
	template <class In> Str(In i, In j) {
		std::copy(i, j, std::back_inserter(data));
	}

	char& operator[](size_type i) { return data[i]; }
	const char& operator[](size_type i) const { return data[i]; }

	size_type size() const { return data.size(); }

	iterator begin() { return data.begin(); }
	const_iterator begin() const { return data.begin(); }

	iterator end() { return data.end(); }
	const_iterator end() const { return data.end(); }

private:
	Vec<char> data;
};

Append operator (+=)

Next, we want to allow for users of our Str class to concatenate two Str objects together. In terms of the implementation, if s, t and v are Str objects, then the expression s = t + v should concatenate the underlying vectors associated with the t and v objects and store the result in s.

Before we implement this function, we first present an overloaded version of the += compound assignment operator. Because this operator modifies the element to the left of the operator, we make this operator method a member of the Str class. The details of this operation are actually relatively simple and the method is implemented as an inline member function:

class Str {
public:
	typedef Vec<char>::size_type size_type;
	typedef char* iterator;
	typedef const char* const_iterator;

	Str() { }
	Str(size_type n, char c): data(n, c) { }
	Str(const char* cp) {
		std::copy(cp, cp + std::strlen(cp), std::back_inserter(data));
	}
	template <class In> Str(In i, In j) {
		std::copy(i, j, std::back_inserter(data));
	}

	char& operator[](size_type i) { return data[i]; }
	const char& operator[](size_type i) const { return data[i]; }

	size_type size() const { return data.size(); }

	iterator begin() { return data.begin(); }
	const_iterator begin() const { return data.begin(); }

	iterator end() { return data.end(); }
	const_iterator end() const { return data.end(); }

	Str& operator+=(const Str& s) {
		std::copy(s.data.begin(), s.data.end(),
				std::back_inserter(data));
		return *this;
	}

private:
	Vec<char> data;
};

Consider the following invocation, where l and r are both Str objects:

l += r;

The operator+=() method takes as its parameter a const reference to the operand on the right-hand side of the += operator (i.e. r). Remember that because this is a member function, the left-hand operand is implicitly passed to this method as the this pointer. Therefore, in the context of this example, this, when used in the operator+=() method will be &l. Also, all accesses to unadorned members such as data will access l's members.

The operator+=() method uses the copy() algorithm from the standard library to append all the characters from the Vec on right-hand side of the operator (i.e. r.data) to the end of the Vec of characters on the left-hand side of the operator (i.e. l.data). Inside the method, r's data vector is accessed via the parameter s and l's data vector is accessed simply as data. We return a reference to the operand on the left-hand side by simply returning *this.

Concatenation operator (+)

With the compound assignment operator defined, we can now easily implement the concatenation operator operator+(). We implement this binary operator as a non-member function since concatenation by itself does not modify either of the operands. Because it is a non-member function, we declare the method in the Str.h header file but outside the definition of the Str class itself.

class Str {
public:
	typedef Vec<char>::size_type size_type;
	typedef char* iterator;
	typedef const char* const_iterator;

	Str() { }
	Str(size_type n, char c): data(n, c) { }
	Str(const char* cp) {
		std::copy(cp, cp + std::strlen(cp), std::back_inserter(data));
	}
	template <class In> Str(In i, In j) {
		std::copy(i, j, std::back_inserter(data));
	}

	char& operator[](size_type i) { return data[i]; }
	const char& operator[](size_type i) const { return data[i]; }

	size_type size() const { return data.size(); }

	iterator begin() { return data.begin(); }
	const_iterator begin() const { return data.begin(); }

	iterator end() { return data.end(); }
	const_iterator end() const { return data.end(); }

	Str& operator+=(const Str& s) {
		std::copy(s.data.begin(), s.data.end(),
				std::back_inserter(data));
		return *this;
	}

private:
	Vec<char> data;
};

// String concatenation operator.
Str operator+(const Str&, const Str&);

This function takes two const Str references as parameters. (If we defined this binary operator as a member function of Str, then like operator+=(), there would only be one explicit parameter — the first parameter would be an implicit parameter which denotes the operand on the left hand side.) The function is defined in the Str.cpp source module using the compound assignment operator as follows:

Str operator+(const Str& s, const Str& t)
{
	Str r = s;
	r += t;
	return r;
}

Note that despite the relative simplicity of this function, there is a lot going on behind the scenes. The Str r = s statement will use Str's (synthesized) copy constructor to initialize the local variable r with s. (Remember, the = symbol in this context means initialization and not assignment.) Then, we invoke the operator+=() method to modify r so that it now contains the result of appending t to the end of s. Finally we return by value the local variable r, which again, calls Str's (synthesized) copy constructor.

As an example of invocations of the concatenation operator, consider the following initialization:

Str greeting = "Hello, " + name + "!";

This initialization takes place in multiple phases. Because the addition operator is left-to-right associative, we first perform the operation "Hello, " + name. This is attempting to add a value of type const char * to a value of type Str. The compiler will consult the available functions to determine which one to use. It then discovers that if it can convert the "Hello ," literal to a code const Str&, then it can call the operator+() function that takes two const Str& parameters. Fortunately, the Str class defines a constructor that will convert a const char* to a Str. Therefore, the compiler first converts the "Hello, " literal to a Str object and stores the result in a temporary:

Str temp1("Hello, ");		// Str::Str(const char *)

Next, the compiler will invoke the operator+() method to create a new temporary Str object which is the result of the concatenation of temp1 with name.

Str temp2 = temp1 + name; 	// operator+(const Str& const Str&)

Then, the compiler will again call the user-defined conversion Str::Str(const char*) to convert the "!" string literal to a Str object and store the result in yet another temporary:

Str temp3("!");			// Str::Str(const char*)

The resulting string that will be used to initialize greeting will then be constructed:

Str greeting = temp2 + temp3; 	// operator+(const Str& const Str&)

Note that greeting is actually created using the synthesized Str::Str(const Str &) copy constructor and not the synthesized assignment operator.

This example demonstrates another reason for not defining operator+() as a member function. The C++ compiler, when determining which operator function to call will consider both its arguments. If it finds a compatible member or non-member function, then it will invoke it. If it doesn't find a compatible function, then it will attempt to do implicit conversions of operands of the operator to see if it can find a compatible function. If it can convert either the left- or right-hand operand to a type which makes both types compatible with a non-member function, then it will use the conversion implicitly and then call the operator function.

However, the compiler will not attempt to do any conversions to the left-hand side of a binary operator that has a corresponding operator method defined as a member function. If we had defined operator+() as a member function of Str, then expressions of the form str + "world" would be valid (if str was of type Str, of course) because the compiler can convert the "world" literal to a Str, then call the operator+() member function. However, the expression "hello" + str, would be invalid even if str was a Str object. The compiler would not attempt to convert the "hello" literal to a Str object and therefore, the compiler would be unable to find an appropriate operator function to call.

Therefore, in order to preserve conversion symmetry, most binary operators that do not modify either operand should be defined as non-member functions.

Relational operators (<, ==, !=, etc.)

The relational operators that we might want to perform on our Str objects are fairly straightforward. They are added to the Str.h header file and are defined as inline non-member functions, for reasons similar to that described in the previous section.

class Str {
public:
	typedef Vec<char>::size_type size_type;
	typedef char* iterator;
	typedef const char* const_iterator;

	Str() { }
	Str(size_type n, char c): data(n, c) { }
	Str(const char* cp) {
		std::copy(cp, cp + std::strlen(cp), std::back_inserter(data));
	}
	template <class In> Str(In i, In j) {
		std::copy(i, j, std::back_inserter(data));
	}

	char& operator[](size_type i) { return data[i]; }
	const char& operator[](size_type i) const { return data[i]; }

	size_type size() const { return data.size(); }

	iterator begin() { return data.begin(); }
	const_iterator begin() const { return data.begin(); }

	iterator end() { return data.end(); }
	const_iterator end() const { return data.end(); }

	Str& operator+=(const Str& s) {
		std::copy(s.data.begin(), s.data.end(),
				std::back_inserter(data));
		return *this;
	}

private:
	Vec<char> data;
};

// String concatenation operator.
Str operator+(const Str&, const Str&);

// Relational operators (note that these are non-member functions).
inline bool operator<(const Str& lhs, const Str& rhs)
{
	return std::lexicographical_compare(lhs.begin(), lhs.end(),
					    rhs.begin(), rhs.end());
}

inline bool operator==(const Str& lhs, const Str& rhs)
{
	return lhs.size() == rhs.size() &&
		std::equal(lhs.begin(), lhs.end(), rhs.begin());
}

inline bool operator!=(const Str& lhs, const Str& rhs)
{
	return !(lhs == rhs);
}

// Other relational operators (e.g. >, <=, >=) can be implemented
// using the above (global) functions.

The operator<() function uses the std::lexicographical_compare() function which takes two pairs of iterators that denote the two ranges to compare. If the range of characters denoted by the first range is lexiocograpically less than the range of characters denoted by the second range, then the function returns true. Otherwise false is returned.

The operator==() equality function makes sure that both supplied Str objects have the same number of chars in their respective vectors, then it uses the standard equal() algorithm function to test if all the corresponding characters are equal. If so, then true is returned; otherwise it returns false. The operator!=() function is implemented as the logical negation of the operator==() function. The remainder of the relational operators may all be implemented in terms of the relational operators given above.

Input/output operators (>>, <<)

Finally, we want to override the >> and << operators so that we can do input and output of Str objects as if they were built-in types in C++. There are a couple of observations which should be made about these functions:

class Str {
	// input operator implemented in 12.3.2/216
	friend std::istream& operator>>(std::istream&, Str&);
public:
	typedef Vec<char>::size_type size_type;
	typedef char* iterator;
	typedef const char* const_iterator;

	Str() { }
	Str(size_type n, char c): data(n, c) { }
	Str(const char* cp) {
		std::copy(cp, cp + std::strlen(cp), std::back_inserter(data));
	}
	template <class In> Str(In i, In j) {
		std::copy(i, j, std::back_inserter(data));
	}

	char& operator[](size_type i) { return data[i]; }
	const char& operator[](size_type i) const { return data[i]; }

	size_type size() const { return data.size(); }

	iterator begin() { return data.begin(); }
	const_iterator begin() const { return data.begin(); }

	iterator end() { return data.end(); }
	const_iterator end() const { return data.end(); }

	Str& operator+=(const Str& s) {
		std::copy(s.data.begin(), s.data.end(),
				std::back_inserter(data));
		return *this;
	}

private:
	Vec<char> data;
};

// String concatenation operator.
Str operator+(const Str&, const Str&);

// output operator implemented in 12.3.2/216
std::ostream& operator<<(std::ostream&, const Str&);

// Relational operators (note that these are non-member functions).
inline bool operator<(const Str& lhs, const Str& rhs)
{
	return std::lexicographical_compare(lhs.begin(), lhs.end(),
					    rhs.begin(), rhs.end());
}
// Other relational operators (e.g. &gt;, &lt;=, &gt;=) can be implemented
// similarly to <code>operator&lt;</code>.

inline bool operator==(const Str& lhs, const Str& rhs)
{
	return lhs.size() == rhs.size() &&
		std::equal(lhs.begin(), lhs.end(), rhs.begin());
}

inline bool operator!=(const Str& lhs, const Str& rhs)
{
	return !(lhs == rhs);
}

Definition of operator<<()

We define the operator<<() function in the Str.cpp source module. This function takes two parameters: the stream to which the output is being sent and a const reference to the Str object being displayed. The implementation is straight-forward — the function simply iterates from the starting index to the end index calling the operator[] member function to access each character of the Str object and writing the character to the output stream. A reference to the output stream is returned as a result of this function.

ostream& operator<<(ostream& os, const Str& s)
{
	for (Str::size_type i = 0; i != s.size(); ++i)
		os << s[i];
	return os;
}

Definition of operator>>()

The definition of the input operator operator>> is a bit more complex. Again, the function takes a reference to the input stream from which the input is acquired and a reference to the Str object that is used to hold the input. The function first clears out any existing characters in the Str object. Because this function is a friend of the Str class, it can access the private data member of Str. The function then executes a loop to skip over any leading whitespace &mdash note the use of the get() method of the input stream to read characters from the input stream one at a time. If the stream is still valid after this loop, then the subsequent non-whitespace characters will be read and appended to the back of the private data vector until we encounter either another whitespace character or until we hit end-of-file. If the last character we read was a whitespace, then we will put it back on the stream using the unget() method of the input stream object. Finally, we return a reference to the input stream as the result of this function.

istream& operator>>(istream& is, Str& s)
{
	// obliterate existing value(s)
	s.data.clear();

	// read and discard leading whitespace
	char c;
	while (is.get(c) && isspace(c))
		;	// nothing to do, except testing the condition

	// if still something to read, do so until next whitespace character
	if (is) {
		do
			// Note that 'data' is private in class 'Str'.
			// Therefore 'istream& operator>>(istream& is, Str& s)'
			// must be declared as a friend in the Str class.
			s.data.push_back(c);
		while (is.get(c) && !isspace(c));

		// if we read whitespace, then put it back on the stream
		if (is)
			is.unget();
	}

	return is;
}

Miscellaneous

Chapter 12 also talks about other features of C++ related to automatic conversions.

Finished Str class and implementation

Str.h header file

#ifndef STR_H
#define STR_H

#include <algorithm>
#include <cstring>
#include <iterator>

#include "Vec.h"

class Str {
	// input operator implemented in 12.3.2/216
	friend std::istream& operator>>(std::istream&, Str&);
public:
	typedef Vec<char>::size_type size_type;
	typedef char* iterator;
	typedef const char* const_iterator;

	Str() { }
	Str(size_type n, char c): data(n, c) { }
	Str(const char* cp) {
		std::copy(cp, cp + std::strlen(cp), std::back_inserter(data));
	}
	template <class In> Str(In i, In j) {
		std::copy(i, j, std::back_inserter(data));
	}

	char& operator[](size_type i) { return data[i]; }
	const char& operator[](size_type i) const { return data[i]; }

	size_type size() const { return data.size(); }

	iterator begin() { return data.begin(); }
	const_iterator begin() const { return data.begin(); }

	iterator end() { return data.end(); }
	const_iterator end() const { return data.end(); }

	Str& operator+=(const Str& s) {
		std::copy(s.data.begin(), s.data.end(),
		          std::back_inserter(data));
		return *this;
	}

private:
	Vec<char> data;
};

// output operator implemented in 12.3.2/216
std::ostream& operator<<(std::ostream&, const Str&);

// String concatenation operator.
Str operator+(const Str&, const Str&);

// Relational operators (note that these are non-member functions).
inline bool operator<(const Str& lhs, const Str& rhs)
{
	return std::lexicographical_compare(lhs.begin(), lhs.end(),
					    rhs.begin(), rhs.end());
}
// Other relational operators (e.g. &gt;, &lt;=, &gt;=) can be implemented
// similarly to <code>operator&lt;</code>.

inline bool operator==(const Str& lhs, const Str& rhs)
{
	return lhs.size() == rhs.size() &&
		std::equal(lhs.begin(), lhs.end(), rhs.begin());
}

inline bool operator!=(const Str& lhs, const Str& rhs)
{
	return !(lhs == rhs);
}

#endif
Str.h

Str.cpp source file

#include <cctype>
#include <iostream>

using std::isspace;

#include "Str.h"

using std::istream;
using std::ostream;

Str operator+(const Str& s, const Str& t)
{
	Str r = s;
	r += t;
	return r;
}

ostream& operator<<(ostream& os, const Str& s)
{
	for (Str::size_type i = 0; i != s.size(); ++i)
		os << s[i];
	return os;
}

istream& operator>>(istream& is, Str& s)
{
	// obliterate existing value(s)
	s.data.clear();

	// read and discard leading whitespace
	char c;
	while (is.get(c) && isspace(c))
		;	// nothing to do, except testing the condition

	// if still something to read, do so until next whitespace character
	if (is) {
		do
			// Note that 'data' is private in class 'Str'.
			// Therefore 'istream& operator>>(istream& is, Str& s)'
			// must be declared as a friend in the Str class.
			s.data.push_back(c);
		while (is.get(c) && !isspace(c));

		// if we read whitespace, then put it back on the stream
		if (is)
			is.unget();
	}

	return is;
}
Str.cpp

Last modified: April 2, 2004 00:55:07 NST (Friday)