prettyPrint that
prints out its arguments nicely. Why would it be
desirable to have prettyPrint use
pass-by-value semantics when passed small
objects like ints and doubles, and pass-by-reference when
passed large objects like classes or struct's with many fields?
When argument expressions are evaluated is (mostly) orthagonal to the issue of what actually gets communicated between the function call and the function body via the parameters.
void mabs(int &x) { if (x < 0) x = -x; }
...
mabs(3-4); ← Trouble: "3-4" isn't an l-value!
The above won't compile in C++. More frustratingly,
neither will:
int mabs(int &x) { return x < 0 ? -x : x; }
...
mabs(3-4);
You see the compiler can't determine whether or not the
body of mabs modifies x. So it conservatively assumes it
does, and we have the same error. In C++ we can work
around it by using "const", which is a promise that we
won't modify something ... a promise that the compiler
holds us too. So, this actually works:
int mabs(const int &x) { return x < 0 ? -x : x; }
...
mabs(3-4);
void bar(Foo a) { a.zeroCounters(); }
...
bar(b);
... does modify the object b, but the call foobar in
Foo x; ... foobar(x);cannot possibly change x to refer to a different object. It can modify the object to which it refers, but never change which object it refers to.
int a = 5; int *p = &a; // at this point "a" and "*p" are aliases: they refer to the same objectAs pointed out earlier, pass by reference gives another way to get aliases:
int a = 5;
int foo(int &x);
int main()
{
f(a); // Inside this call to foo, "x" refers to the same object as "a", i.e. we have aliasing.
Actually, C++ allows you to make explicit aliases like this:
int a = 5; int &x = a;This defines x as an alias for a. Why would anyone ever want to do that? Well you often get big ugly expressions for things that you need to
string &var = root->child[0]->idVal; Frame &T = *frame; if (T.find(var) != T.end()) T[var] = RHS;Alias "T" might be gratuitous, but alias "var" really does make things easier to read.
bool foo(int x) { return x > 5; }
bool foo(string &s) { return s.length() > 5; }
bool foo(int *p) { return *p > 5; }
int main()
{
int a = 7;
string u;
foo(7); // get foo(int)
foo(&a); // get foo(int*)
foo(u); // get foo(string)
Notice that at each call site we determine "which foo" we
actually get based on the argument type. Operators like + and
* are usually overloaded in the same way: int+int is different
from string+string, for example. It's important to note that
type coercion can often make it look like there is
function overloading when there's not, e.g.:
int a = -7; double x = -7.3; cout << abs(a) << endl; cout << abs(x) << endl;This compiles just fine. You might think
abs is overloaded for both int
and double types ... but you're wrong. It only exists for int
arguments, it's just that the double agrument x is
coerced to an int, so that abs(int) can
be used. You'll see that when the code runs and the output is
two sevens, not 7 followed by 7.3. This particular "feature"
nearly brought me to tears while I was working on the first
programming project of my numerical analysis course (my first
exposure to C). Type coercion is not overloading, because
there is only one abs function ... doubles merely
undergo amputation to they fit.
Pay attention: In statically typed languages overloaded function calls are resolved (i.e. which exact function you get is decided on) at compile time!
| compile & run | ex10.cpp |
> g++ ex10.cpp > ./a.out Bar:0x8047500 Foo:0x8047510 Bar:0x8047500 Bar:0x8047500 Foo:0x8047510 Bar:0x8047500 Bar:0x8047500 Bar:0x8047500 Foo:0x8047510 Bar:0x8047500 |
#include <iostream>
using namespace std;
class Adam
{
public:
virtual void print() const { cout << "Adam:" << this << endl; }
};
class Foo : public Adam
{
public:
void print() const { cout << "Foo:" << this << endl; }
};
class Bar : public Adam
{
public:
void print() const { cout << "Bar:" << this << endl; }
};
int main()
{
srand(time(0));
Foo a;
Bar b;
for(int i = 0; i < 10; ++i)
{
Adam *p;
if (rand()%2) p = &a; else p = &b;
p->print();
}
}
|
The interesting thing here is the call site
p->print() (yes, it's still just a function
call!). As the output plainly shows, this same call site
can result in different actual functions being called. This
is very different from overloading. When a single call site
can result in different concrete functions getting called,
we say it is a polymorphic function call. The kind
of polymorphism in this example is what's called subtype
polymorphism. Function "print" is a member of the base
class Adam. Because Foo and Bar are derived from Adam, "a"
and "b" are Adam types. Therefore Adam
*p can point to them. At the site p->print(),
there are two options: you could get the print() defined in
Adam, because p is a pointer to an Adam, or you could get
the print() defined in Foo/Bar depending on which p happens
to be pointing to. In the first case, function overloading
is at work, in the second polymorphism. The "virtual"
keyword in C++ tells the compiler to treat calls to print()
polymorphically. In Java all calls default to polymorphic calls.
Pay attention: Even in statically typed languages, polymorphic function calls cannot, in general, be resolved (i.e. which exact function you get decided upon) until run time!