Menu

SI413 Class 23: function call to function body


Reading
Section 8.3 and 3.6 of Programming Language Pragmatics.

Homework
  1. Look at the following bit of java code. Print it out and draw an arrow from each call site to the definition for the function the call resolves to, i.e. that actually gets called. Also mark each call as either "overloaded" or "polymorphic" depending on whether the call merely requires overloading to resolve or whether it's a truly polymorphic call.
  2. Suppose you have a function 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?

Actual parameters to formal parameters
The last class looked at macros and noted that they gave us a different view of how actual parameters at the call site get mappped to formal parameters inside the function function body. In the distinction between applicative order and normal order is when argument expressions are evaluated. The referencing environment in which they are evaluated is always that of the call site, but whereas applicative order evaluates all function argument arguments before entering the function body, normal order evaluates argument expressions as needed during the evaluation of the function body (this processed is memoized so no argument expression is actually evaluated more than once, even if it's needed multiple times in the function evaluation).

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.

pass-by-value
This is what you're used to in C/C++ and Java with int/double/char. It's natural with the value model of variables. Formal parameters get bound to copies of the actual parameters.

pass-by-reference
Here the formal parameters are aliases for the actual parameters. That means they are bound to the same objects. C++ is the only place that we have seen true pass-by-reference in our curriculum. Take a look at this example, which shows pass-by-reference in C++. Anyway, call by reference has two advantages over pass-by-value: 1) you can modify objects from the call-site in the function, 2) when arguments are large objects, you avoid making copies. One subtle issue is that if a function body uses a pass-by-reference parameter as an l-value, the actual parameter better be an l-value as well.
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);

pass-by-sharing
Pass-by-sharing is just pass-by-value in a refernce model environment. The reference is passed by value, but that means the formal parameter references the same object as the actual parameter. This is what Java uses with anything derived from Object. As a result:
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.

pass-by-name
This is what you get with macros. We talked about it last class.

Aliases: one thing with many names
The issue of aliasing - when one object has many names - is important in programming languages and has a big impact on what opimizing compilers can do. Here's a classic way to get aliases in C++:
int a = 5;
int *p = &a;
// at this point "a" and "*p" are aliases: they refer to the same object
As 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.

Overloading: one name refers to many things
Overloading allows many functions to share a name. Which function a given call site refers to is determined by matching the number and types of actual parameters with the number and types of formal parameters.
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!

Polymorphism
With overloading, at least in statically typed languages, a given call site refers to one and only one actual function: it's just that you need to pay attention to the names and types of arguments to figure out which one. With our next construct, polymorphism, there are call sites that may result in different functions being executed from run to run ... or even within the same run. Consider this:
compile & runex10.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!


Christopher W Brown
Last modified: Tue Dec 8 13:57:42 EST 2009