WIP operator overloading

git-svn-id: https://cppannotations.svn.sourceforge.net/svnroot/cppannotations/trunk@312 f6dd340e-d3f9-0310-b409-bdd246841980
This commit is contained in:
Frank B. Brokken 2009-12-04 16:02:00 +00:00
parent 525d470554
commit b5a556f01e
2 changed files with 70 additions and 128 deletions

View file

@ -9,12 +9,13 @@ lsect(EXTRACTORS)(Overloading the insertion and extraction operators)
includefile(overloading/insertextract)
COMMENT(>>>>>>>>>>>>> NEXT <<<<<<<<<<<<<)
lsect(EXPLICIT)(The keyword `explicit')
includefile(overloading/explicit.yo)
lsect(ConversionOperators)(Conversion operators)
includefile(overloading/conversion)
COMMENT(>>>>>>> Done <<<<<<<<)
lsect(EXPLICIT)(The keyword `explicit')
includefile(overloading/explicit.yo)
subsect(Explicit conversion operators (C++0x))
includefile(overloading/explicitconversion)

View file

@ -1,133 +1,74 @@
COMMENT(<<<<<<<<< FROM MEMORY: PROMOTIONS
Conversions are not only performed by conversion operators, but also by
constructors accepting one argument (i.e., constructors having one or multiple
parameters, specifying default argument values for all parameters or for all
but the first parameter).
As an other example, once again assuming the availability of a
tt(Person(char const *name)) constructor, consider:
verb(
Person namedPerson()
{
string name;
Assume a data base class tt(DataBase) is defined in which tt(Person)
objects can be stored. It defines a tt(Person *d_data) pointer, and so it
offers a copy constructor and an overloaded assignment operator.
cin >> name;
return name.c_str();
}
)
Here, even though the return value tt(name.c_str()) doesn't match the
return type tt(Person), there is a em(constructor) available to construct a
tt(Person) from a tt(char const *). Since such a constructor is available, the
(anonymous) return value can be constructed by em(promoting) a
hi(promoting a type) tt(char const *) type to a tt(Person) type using an
appropriate constructor.
>>>>>>>>>)
Conversions are performed not only by conversion operators, but also by
i(constructors having one parameter) (or multiple parameters, having
i(default argument values) beyond the first parameter).
Consider the class tt(Person) introduced in chapter ref(MEMORY). This
class has a constructor
centt(Person(char const *name, char const *address, char const *phone))
This constructor could be given default argument values:
verb(
Person(char const *name, char const *address = "<unknown>",
char const *phone = "<unknown>");
)
In several situations this constructor might be used intentionally,
possibly providing the default tt(<unknown>) texts for the address and phone
numbers. For example:
verb(
Person frank("Frank", "Room 113", "050 363 9281");
)
Also, functions might use tt(Person) objects as parameters, e.g., the
following member in a fictitious class tt(PersonData) could be available:
verb(
PersonData &PersonData::operator+=(Person const &person);
)
Now, combining the above two pieces of code, we might, do something like
verb(
PersonData dbase;
dbase += frank; // add frank to the database
)
So far, so good. However, since the tt(Person) constructor can also be
used as a conversion operator, it is em(also) possible to do:
verb(
dbase += "karel";
)
Here, the tt(char const *) text `tt(karel)' is converted to an (anonymous)
tt(Person) object using the abovementioned tt(Person) constructor: the second
and third parameters use their i(default value)s. Here, an
emi(implicit conversion) is performed from a tt(char const *) to a
tt(Person) object, which might not be what the programmer had in mind when the
class tt(Person) was constructed.
As another example, consider the situation where a class representing a
container is constructed. Let's assume that the initial construction of
objects of this class is rather complex and time-consuming, but em(expanding)
an object so that it can accomodate more elements is even more
time-consuming. Such a situation might arise when a hash-table is initially
constructed to contain tt(n) elements: that's OK as long as the table is not
full, but when the table must be expanded, all its elements normally must be
rehashed to allow for the new table size.
Such a class could (partially) be defined as follows:
verb(
class HashTable
{
size_t d_maxsize;
public:
HashTable(size_t n); // n: initial table size
size_t size(); // returns current # of elements
// add new key and value
void add(std::string const &key, std::string const &value);
};
)
Now consider the following implementation of tt(add()):
verb(
void HashTable::add(string const &key, string const &value)
{
if (size() > d_maxsize * 0.75) // table gets rather full
*this = size() * 2; // Oops: not what we want!
// etc.
}
)
In the first line of the body of tt(add()) the programmer first determines
how full the hashtable currently is: if it's more than three quarter full,
then the intention is to double the size of the hashtable. Although this
succeeds, the hashtable will completely fail to fulfill its purpose:
accidentally the programmer assigns an size_t value, intending to tell the
hashtable what its new size should be. This results in the following unwelcome
surprise:
In addition to the copy constructor tt(DataBase) offers a default
constructor and several additional constructors:
itemization(
it() The compiler notices that no tt(operator=(size_t newsize)) is
available for tt(HashTable).
it() There is, however, a constructor accepting an size_t, em(and) the
default overloaded assignment operator is still available, expecting a
tt(HashTable) as its right-hand operand.
it() Thus, the rvalue of the assignment (a tt(HashTable)) is obtained by
(implicitly) constructing an (empty) tt(HashTable) that can accomodate
tt(size() * 2) elements.
it() The just constructed empty tt(HashTable) is thereupon assigned to the
current tt(HashTable), thus em(removing all hitherto stored elements from the
current tt(HashTable)).
itt(DataBase(Person const &)): the tt(DataBase) initially contains a
single tt(Person) object;
itt(DataBase(istream &in)): the data about multiple persons are read from
tt(in).
itt(DataBase(size_t count, istream &in = cin)): the data of tt(count)
persons are read from tt(in), by default the standard input stream.
)
If an hi(constructor: implicit use) implicit use of a constructor is not
appropriate (or dangerous), it can be prevented using the ti(explicit)
modifier with the constructor. Constructors using the tt(explicit) modifier
can only be used for the i(explicit construction) of objects, and cannot be
used as implicit type convertors anymore. For example, to prevent the implicit
conversion from tt(size_t) to tt(HashTable) the class interface of the
class tt(HashTable) should declare the constructor
The above constructors all are perfectly reasonable. But they also allow
the compiler to compile the following code without producing any warning at
all:
verb(
explicit HashTable(size_t n);
DataBase db;
DataBase db2;
Person person;
db2 = db; // 1
db2 = person; // 2
db2 = 10; // 3
db2 = cin; // 4
)
Now the compiler will catch the error in the compilation of
tt(HashTable::add()), producing an error message like
Statement 1 is perfectly reasonable: tt(db) is used to redefine
tt(db2). Statement 2 might be understandable since we designed tt(DataBase) to
contain tt(Person) objects. Nevertheless, we might question the logic that's
used here as a tt(Person) is not some kind of tt(DataBase). The logic becomes
even more opaque when looking at statements 3 and 4. Statement 3 in effect
will wait for the data of 10 persons to appear at the standard input
stream. Nothing like that is suggested by tt(db2 = 10).
All four statements are the result of implicit
i(promotion)s. Since constructors accepting, respectively a tt(Person), a
tt(size_t) and an tt(istream) have been defined for tt(PersonData) and since
the assignment operator expects a tt(PersonData) right-hand side (rhs)
argument the compiler will first convert the rhs arguments to anonymous
tt(DataBase) objects which are then assigned to tt(db2).
It is good practice to prevent implicit promotions by using the
ti(explicit) modifier when declaring a constructor. Constructors using the
tt(explicit) modifier can only be used to construct objects
explicitly. Statements 1-4 would not have compiled if the constructors
expecting one argument would have been declared using tt(explicit). E.g.,
verb(
error: no match for 'operator=' in
'*this = (this->HashTable::size()() * 2)'
explicit DataBase(Person const &person);
explicit DataBase(size_t count, std:istream &in);
)
Having declared all constructors accepting one argument as tt(explicit)
the above assignments would have required the explicit specification of
the appropriate constructors, thus clarifying the programmer's intent:
verb(
DataBase db;
DataBase db2;
Person person;
db2 = db; // 1
db2 = DataBase(person); // 2
db2 = DataBase(10); // 3
db2 = DataBase(cin); // 4
)
As a i(rule of thumb) prefix one argument constructors with the
tt(explicit) keyword unless implicit promotions are perfectly natural
(tt(string)'s tt(char const *) accepting constructor is a case in point).