|














Am avut 371731 vizite de la lansarea siteului.

|
|
Inapoi
Inainte
Cuprins
Arguments & return
values
It may seem a little confusing at first
when you look at OverloadingUnaryOperators.cpp, Integer.h and
Byte.h and see all the different ways that arguments are passed and
returned. Although you can pass and return arguments any way you want to,
the choices in these examples were not selected at random. They follow a logical
pattern, the same one you’ll want to use in most of your
choices.
- As with any function
argument, if you only need to read from the argument and not change it, default
to passing it as a const reference. Ordinary arithmetic operations (like
+ and –, etc.) and Booleans will not change their arguments,
so pass by const reference is predominantly what you’ll use. When
the function is a class member, this translates to making it a const
member function. Only with the operator-assignments (like +=) and the
operator=, which change the left-hand argument, is the left argument
not a constant, but it’s still passed in as an address because it
will be changed.
- The
type of return value you should select depends on the expected meaning of the
operator. (Again, you can do anything you want with the arguments and return
values.) If the effect of the operator is to produce a new value, you will need
to generate a new object as the return value. For example,
Integer::operator+ must produce an Integer object that is the sum
of the operands. This object is returned by value as a const, so the
result cannot be modified as an
lvalue.
- All the
assignment operators modify the lvalue. To allow the result of the assignment to
be used in chained expressions, like a=b=c, it’s expected that you
will return a reference to that same lvalue that was just modified. But should
this reference be a const or nonconst? Although you read
a=b=c from left to right, the compiler parses it from right to left, so
you’re not forced to return a nonconst to support assignment
chaining. However, people do sometimes expect to be able to perform an operation
on the thing that was just assigned to, such as (a=b).func( ); to
call func( ) on a after assigning b to it. Thus, the
return value for all of the assignment operators should be a nonconst
reference to the
lvalue.
- For the
logical operators, everyone expects to get at worst an int back, and at
best a bool. (Libraries developed before most compilers supported
C++’s built-in bool will use int or an equivalent
typedef.)
The increment
and decrement operators
present
a dilemma because of the pre- and postfix versions. Both versions change the
object and so cannot treat the object as a const. The prefix version
returns the value of the object after it was changed, so you expect to get back
the object that was changed. Thus, with prefix you can just return *this
as a reference. The postfix version is supposed to return the value
before the value is changed, so you’re forced to create a separate
object to represent that value and return it. So with postfix you must return by
value if you want to preserve the expected meaning. (Note that you’ll
sometimes find the increment and decrement operators returning an int or
bool to indicate, for example, whether an object designed to move through
a list is at the end of that list.) Now the question is: Should these be
returned as const or nonconst? If you allow the object to be
modified and someone writes (++a).func( ), func( ) will
be operating on a itself, but with (a++).func( ),
func( ) operates on the temporary object returned by the postfix
operator++. Temporary objects are automatically const, so this
would be flagged by the compiler, but for consistency’s sake it may make
more sense to make them both const, as was done here. Or you may choose
to make the prefix version non-const and the postfix const.
Because of the variety of meanings you may want to give the increment and
decrement operators, they will need to be considered on a case-by-case
basis.
Return by value as
const
Returning by value as a const can
seem a bit subtle at first, so it deserves a bit more explanation. Consider the
binary operator+. If you use it in an expression such as f(a+b),
the result of a+b becomes a temporary object that is used in the call to
f( ). Because it’s a temporary, it’s automatically
const, so whether you explicitly make the return value const or
not has no effect.
However, it’s also possible for you
to send a message to the return value of a+b, rather than just passing it
to a function. For example, you can say (a+b).g( ), in which
g( ) is some member function of Integer, in this case. By
making the return value const, you state that only a const member
function can be called for that return value. This is const-correct,
because it prevents you from storing potentially valuable information in an
object that will most likely be lost.
The return
optimization
When new objects are created to return by
value, notice the form used. In operator+, for example:
return Integer(left.i + right.i);
This may look at first like a
“function call to a constructor,” but it’s not. The syntax is
that of a temporary object; the statement says “make a temporary
Integer object and return it.” Because of this, you might think
that the result is the same as creating a named local object and returning that.
However, it’s quite different. If you were to say
instead:
Integer tmp(left.i + right.i);
return tmp;
three things will happen. First, the
tmp object is created including its constructor call. Second, the
copy-constructor copies the tmp to the location
of the outside return value. Third, the destructor is called for tmp at
the end of the scope.
In contrast, the “returning a
temporary” approach works quite differently. When
the compiler sees you do this, it knows that you have no other need for the
object it’s creating than to return it. The compiler takes advantage of
this by building the object directly into the location of the outside
return value. This requires only a single ordinary constructor call (no
copy-constructor is necessary) and there’s no destructor call because you
never actually create a local object. Thus, while it doesn’t cost anything
but programmer awareness, it’s significantly more efficient. This is often
called the return value
optimization.
Unusual operators
Several additional operators have a
slightly different syntax for overloading.
The subscript, operator[
], must be a member function and it requires a
single argument. Because operator[ ] implies that the object it’s
being called for acts like an array, you will often return a reference from this
operator, so it can be conveniently used on the left-hand side of an equal sign.
This operator is commonly overloaded; you’ll see examples in the rest of
the book.
The operators new and
delete control dynamic storage allocation and can be overloaded in a
number of different ways. This topic is covered in the Chapter
13.
Operator comma
The comma
operator is called when it
appears next to an object of the type the comma is defined for. However,
“operator,” is not called for function argument
lists, only for objects that are out in the open, separated by commas. There
doesn’t seem to be a lot of practical uses for this operator; it’s
in the language for consistency. Here’s an example showing how the comma
function can be called when the comma appears before an object, as well
as after:
//: C12:OverloadingOperatorComma.cpp
#include <iostream>
using namespace std;
class After {
public:
const After& operator,(const After&) const {
cout << "After::operator,()" << endl;
return *this;
}
};
class Before {};
Before& operator,(int, Before& b) {
cout << "Before::operator,()" << endl;
return b;
}
int main() {
After a, b;
a, b; // Operator comma called
Before c;
1, c; // Operator comma called
} ///:~
The global function allows the comma to
be placed before the object in question. The usage shown is fairly obscure and
questionable. Although you would probably use a comma-separated list as part of
a more complex expression, it’s too subtle to use in most
situations.
Operator->
The
operator–>
is generally used when you want to make an object appear to be a pointer. Since
such an object has more “smarts” built into it than exist for a
typical pointer, an object like this is often called a smart pointer.
These are especially useful if you want to “wrap” a class around a
pointer to make that pointer safe, or in the common usage of an
iterator, which is an object that moves through a
collection /container
of other objects and selects them one at a time,
without providing direct access to the implementation of the container.
(You’ll often find containers and iterators in class libraries, such as in
the Standard C++ Library, described in Volume 2 of this book.)
A pointer dereference operator must be a
member function. It has additional, atypical constraints: It must return an
object (or reference to an object) that also has a pointer dereference operator,
or it must return a pointer that can be used to select what the pointer
dereference operator arrow is pointing at. Here’s a simple
example:
//: C12:SmartPointer.cpp
#include <iostream>
#include <vector>
#include "../require.h"
using namespace std;
class Obj {
static int i, j;
public:
void f() const { cout << i++ << endl; }
void g() const { cout << j++ << endl; }
};
// Static member definitions:
int Obj::i = 47;
int Obj::j = 11;
// Container:
class ObjContainer {
vector<Obj*> a;
public:
void add(Obj* obj) { a.push_back(obj); }
friend class SmartPointer;
};
class SmartPointer {
ObjContainer& oc;
int index;
public:
SmartPointer(ObjContainer& objc) : oc(objc) {
index = 0;
}
// Return value indicates end of list:
bool operator++() { // Prefix
if(index >= oc.a.size()) return false;
if(oc.a[++index] == 0) return false;
return true;
}
bool operator++(int) { // Postfix
return operator++(); // Use prefix version
}
Obj* operator->() const {
require(oc.a[index] != 0, "Zero value "
"returned by SmartPointer::operator->()");
return oc.a[index];
}
};
int main() {
const int sz = 10;
Obj o[sz];
ObjContainer oc;
for(int i = 0; i < sz; i++)
oc.add(&o[i]); // Fill it up
SmartPointer sp(oc); // Create an iterator
do {
sp->f(); // Pointer dereference operator call
sp->g();
} while(sp++);
} ///:~
The class Obj defines the objects
that are manipulated in this program. The functions f( ) and
g( ) simply print out interesting values using static data
members. Pointers to these objects are stored inside containers of type
ObjContainer using its add( ) function. ObjContainer
looks like an array of pointers, but you’ll notice there’s no way to
get the pointers back out again. However, SmartPointer is declared as a
friend class, so it has permission to look inside the container. The
SmartPointer class looks very much like an intelligent pointer –
you can move it forward using operator++ (you can also define an
operator– –), it won’t go past the end of the container
it’s pointing to, and it produces (via the pointer dereference operator)
the value it’s pointing to. Notice that the SmartPointer is a
custom fit for the container it’s created for; unlike an ordinary pointer,
there isn’t a “general purpose” smart pointer. You will learn
more about the smart pointers called “iterators” in the last chapter
of this book and in Volume 2 (downloadable from
www.BruceEckel.com).
In main( ), once the
container oc is filled with Obj objects, a SmartPointer sp
is created. The smart pointer calls happen in the expressions:
sp->f(); // Smart pointer calls
sp->g();
Here, even though sp doesn’t
actually have f( ) and g( ) member functions, the
pointer dereference operator automatically calls those functions for the
Obj* that is returned by SmartPointer::operator–>. The
compiler performs all the checking to make sure the function call works
properly.
Although the underlying mechanics of the
pointer dereference operator are more complex than the other operators, the goal
is exactly the same: to provide a more convenient syntax for the users of your
classes.
A nested
iterator
It’s more common to see a
“smart pointer” or “iterator” class nested within the
class that it services. The previous example can be rewritten to nest
SmartPointer inside ObjContainer like this:
//: C12:NestedSmartPointer.cpp
#include <iostream>
#include <vector>
#include "../require.h"
using namespace std;
class Obj {
static int i, j;
public:
void f() { cout << i++ << endl; }
void g() { cout << j++ << endl; }
};
// Static member definitions:
int Obj::i = 47;
int Obj::j = 11;
// Container:
class ObjContainer {
vector<Obj*> a;
public:
void add(Obj* obj) { a.push_back(obj); }
class SmartPointer;
friend SmartPointer;
class SmartPointer {
ObjContainer& oc;
unsigned int index;
public:
SmartPointer(ObjContainer& objc) : oc(objc) {
index = 0;
}
// Return value indicates end of list:
bool operator++() { // Prefix
if(index >= oc.a.size()) return false;
if(oc.a[++index] == 0) return false;
return true;
}
bool operator++(int) { // Postfix
return operator++(); // Use prefix version
}
Obj* operator->() const {
require(oc.a[index] != 0, "Zero value "
"returned by SmartPointer::operator->()");
return oc.a[index];
}
};
// Function to produce a smart pointer that
// points to the beginning of the ObjContainer:
SmartPointer begin() {
return SmartPointer(*this);
}
};
int main() {
const int sz = 10;
Obj o[sz];
ObjContainer oc;
for(int i = 0; i < sz; i++)
oc.add(&o[i]); // Fill it up
ObjContainer::SmartPointer sp = oc.begin();
do {
sp->f(); // Pointer dereference operator call
sp->g();
} while(++sp);
} ///:~
Besides the actual nesting of the class,
there are only two differences here. The first is in the
declaration
of the class so that it can be a friend:
class SmartPointer;
friend SmartPointer;
The compiler must first know that the
class exists before it can be told that it’s a
friend.
The second difference is in the
ObjContainer member function begin( ), which produces a
SmartPointer that points to the beginning of the ObjContainer
sequence. Although it’s really only a convenience, it’s valuable
because it follows part of the form used in the Standard C++
Library.
Operator->*
The operator–>*
is a binary operator that
behaves like all the other binary operators. It is provided for those situations
when you want to mimic the behavior provided by the built-in
pointer-to-member syntax, described in the
previous chapter.
Just like operator->, the
pointer-to-member dereference operator is generally used with some kind of
object that represents a “smart pointer,” although the example shown
here will be simpler so it’s understandable. The trick when defining
operator->* is that it must return an object for which the
operator( ) can be called with the arguments for the member function
you’re calling.
The function call
operator( )
must be a member function, and it is unique in that it allows any number of
arguments. It makes your object look like it’s actually a function.
Although you could define several overloaded operator( ) functions
with different arguments, it’s often used for types that only have a
single operation, or at least an especially prominent one. You’ll see in
Volume 2 that the Standard C++ Library uses the function call operator in order
to create “function objects.”
To create an operator->* you
must first create a class with an operator( ) that is the type of
object that operator->* will return. This class must somehow capture
the necessary information so that when the operator( ) is called
(which happens automatically), the pointer-to-member will be dereferenced for
the object. In the following example, the FunctionObject constructor
captures and stores both the pointer to the object and the pointer to the member
function, and then the operator( ) uses those to make the actual
pointer-to-member call:
//: C12:PointerToMemberOperator.cpp
#include <iostream>
using namespace std;
class Dog {
public:
int run(int i) const {
cout << "run\n";
return i;
}
int eat(int i) const {
cout << "eat\n";
return i;
}
int sleep(int i) const {
cout << "ZZZ\n";
return i;
}
typedef int (Dog::*PMF)(int) const;
// operator->* must return an object
// that has an operator():
class FunctionObject {
Dog* ptr;
PMF pmem;
public:
// Save the object pointer and member pointer
FunctionObject(Dog* wp, PMF pmf)
: ptr(wp), pmem(pmf) {
cout << "FunctionObject constructor\n";
}
// Make the call using the object pointer
// and member pointer
int operator()(int i) const {
cout << "FunctionObject::operator()\n";
return (ptr->*pmem)(i); // Make the call
}
};
FunctionObject operator->*(PMF pmf) {
cout << "operator->*" << endl;
return FunctionObject(this, pmf);
}
};
int main() {
Dog w;
Dog::PMF pmf = &Dog::run;
cout << (w->*pmf)(1) << endl;
pmf = &Dog::sleep;
cout << (w->*pmf)(2) << endl;
pmf = &Dog::eat;
cout << (w->*pmf)(3) << endl;
} ///:~
Dog has three member functions,
all of which take an int argument and return an int. PMF is
a typedef to simplify defining a pointer-to-member to Dog’s
member functions.
A FunctionObject is created and
returned by operator->*. Notice that operator->* knows both
the object that the pointer-to-member is being called for (this) and the
pointer-to-member, and it passes those to the FunctionObject constructor
that stores the values. When operator->* is called, the compiler
immediately turns around and calls operator( ) for the return value
of operator->*, passing in the arguments that were given to
operator->*. The FunctionObject::operator( ) takes the
arguments and then dereferences the “real” pointer-to-member using
its stored object pointer and pointer-to-member.
Notice that what you are doing here, just
as with operator->, is inserting yourself in the middle of the call to
operator->*. This allows you to perform some extra operations if you
need to.
The operator->* mechanism
implemented here only works for member functions that take an int
argument and return an int. This is limiting, but if you try to
create overloaded mechanisms for each different possibility, it seems like a
prohibitive task. Fortunately, C++’s template mechanism (described
in the last chapter of this book, and in Volume 2) is designed to handle just
such a
problem.
Operators you can’t
overload
There are certain operators in the
available set that cannot be overloaded. The general reason for the restriction
is safety. If these operators were overloadable, it would somehow jeopardize or
break safety mechanisms, make things harder, or confuse existing
practice.
- The member selection
operator.. Currently, the dot has a meaning for any member in a class,
but if you allow it to be overloaded, then you couldn’t access members in
the normal way; instead you’d have to use a pointer and the arrow
operator->.
- The
pointer to member dereference operator.*, for the same reason as
operator..
- There’s
no exponentiation operator. The
most popular choice for this was operator** from Fortran, but this raised
difficult parsing questions. Also, C has no exponentiation operator, so C++
didn’t seem to need one either because you can always perform a function
call. An exponentiation operator would add a convenient notation, but no new
language functionality to account for the added complexity of the
compiler.
- There are
no user-defined operators. That is, you can’t make
up new operators that aren’t currently in the set. Part of the problem is
how to determine precedence, and part of the problem is an insufficient need to
account for the necessary
trouble.
- You
can’t change the precedence rules. They’re hard enough to remember
as it is without letting people play with
them.
 |
| |
|