March 05 (Friday) March 10 (Wednesday)
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
.
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
ConstructorsThe above class definition also defines four different constructors:
Vec
class, which, by default
creates an empty vector:
Str() { }
n
and a character
c
and creates a string that consists of n
copies of c
. Again, it relies on a corresponding constructor
of the Vec
class to do the initialization. Note the use
of the data
member in the class initialization portion
of the constructor:
Str(size_type n, char c): data(n, c) { }
Str
object using a pointer to a nul terminated array of characters.
This constructor uses the copy()
function from the
<algorithm>
header. Note that we are using the
character pointer cp
as if it were an iterator:
Str(const char* cp) { std::copy(cp, cp + std::strlen(cp), std::back_inserter(data)); }
This is a special type of constructor and is sometimes referred to as a user-defined conversion. Such a constructor takes a single argument and will, in effect convert that argument to an object of the class type in which it is defined. We will see how this constructor is used in later sections.
Str
object by copying characters from the range denoted by the two iterators
into the data
buffer.
template <class In> Str(In i, In j) { std::copy(i, j, std::back_inserter(data)); }
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 delete
s, then the Str
class
would have to implement appropriate versions of the copy constructor,
destructor and assignment operator.
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; };
+=
)
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
.
+
)
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.
<
, ==
, !=
, 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 char
s
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.
>>
, <<
)
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:
istream
and
ostream
classes. Therefore, we must define these operator
functions as as non-member functions. Both these functions will
therefore be declared in the Str.h
header file and defined
in the Str.cpp
source module.
>>
needs access to the
private data
member of the Str
object that is
being used to store the input. However, we've already said that we cannot
make operator>>()
a member function. Fortunately,
C++ provides a way to grant a non-member function access to a class's
private data using the friend
keyword. We declare the
operator>>()
function as a friend
in the
Str
class declaration, as demonstrated by the following code.
The operator<<()
function, which does not need special
access is simply declared outside the Str
class. 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. >, <=, >=) can be implemented // similarly to <code>operator<</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); }
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; }
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; }
Chapter 12 also talks about other features of C++ related to automatic conversions.
Str::Str(const char*)
), a class can also tell the compiler
how to convert from the class type to another type by providing a
member function of the form operator
type()
const
(§12.5). The implementation of this function describes
how to convert an object of the class to the type type.
For example, the Student_info
class that we defined earlier
could define a function operator double() const { ... }
which describes how to convert a Student_info
object to a
double, which could represent the overall grade for the student.
The same section of the text also describes the conversions that
are used when an istream
object is used in a boolean context.
explicit
keyword introduced in the previous chapter.
Its purpose is to prevent constructors that denote user-defined
conversions from being called implicitly. The book gives the following
advice on page 221:
In general, it is useful to make
explicit
the constructors that define the structure of the object being constructed, rather than its contents.
In our definition of the Vec
class last day, we
made the following constructor
explicit Vec(size_type n, const T& t = T()) { ... }
explicit
because the single compulsory parameter
(n
) denotes the structure of the vector (i.e. that
it is to allocate room for n
elements) rather than denotes
the vector's content. If this constructor was not explicit, then a statement of
the form:
Vec<int> v = 42;
would implicitly (and probably mistakenly, from the point of view of the
programmer) create a vector that contained 42 elements — it would
not create a vector that contained the number 42 as one of its elements.
With the explicit
keyword in place, the above statement
would generate a compile-time error.
Str
class and implementationStr.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. >, <=, >=) can be implemented
// similarly to <code>operator<</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)