CS 330: Lecture 16 – Ad Hoc Polymorphism Continued
Dear students,
As we discussed last time, polymorphism is the pursuit of code that works with many types. It manifests itself in several forms that we’ve been discussing:
- Coercion, in which we have a value of type
X
and an operation that expects typeY
, but there’s a known path for convertingX
s intoY
s. The function is polymorphic in the sense that it can operate onY
s and any values of types that can be converted toY
. - Ad hoc polymorphism is the name we give to the overloading of functions. Several versions of a single function are written, each catering to a different type. We don’t really save on effort with ad hoc polymorphism, but we at least save on names. Each version of that function has the same name.
We started discussing ad hoc polymorphism last time, and we overloaded operator[]
for our super fast string class. It made our string feel a lot like an array. Or does it? What happens here?
Zing z = "goliath";
z[0] = 'G';
To make the character writable, we can return a reference instead of a temporary:
char &operator[](int i) {
return text[i];
}
It’s common practice in C++ to provide two versions of methods, one that returns a reference and another that is const
and returns a plain old value.
Now suppose we want to print the whole string just like we can print other things in C++:
std::cout << z << std::endl;
What do we want to overload here? A method of cout
? The rules of C++ state that the compiler will look first for something like this:
ostream &ostream::operator<<(const Zing &z);
But there’s no way we can sneak into the ostream
class and an overloaded definition for our new class. However, if this method can’t be found, the compiler then looks for a method like this:
ostream &operator<<(ostream &stream, const Zing &z);
The difference here is that operator<<
is not owned by any class. It’s a top-level definition. We can write it like so:
std::ostream &operator<<(std::ostream &stream, const Zing &z) {
for (int i = 0; i < z.Length(); ++i) {
stream << z[i];
}
return stream;
}
Overloading functions still requires us to write a version for each different type we want to support, which is probably why it’s called “ad hoc”—which is Latin for “for this.” You write a version for this type, and for this other type, and for this even more other type… But from the client perspective, there’s just one function.
How does overloading work in Javascript? Or Ruby? Let’s check. Here’s a hypot
method that supports either two or three dimensions:
function hypot(a, b, c) {
return Math.sqrt(a * a + b * b + c * c);
}
function hypot(a, b) {
return Math.sqrt(a * a + b * b);
}
console.log(hypot(3, 4));
console.log(hypot(1, 4, 8));
Does this give the output we expect? Not at all. Because Javascript doesn’t support function overloading. Nor does Ruby. Nor does Python. The name is bound to only a single definition. Which one? Whichever definition appeared last.
In these languages, we have a few options to emulate overloading. We can combine the multiple definitions into an if
-ladder and inspect the parameters to arbitrate between them. In Javascript, we can examine the length of the implicit arguments
array:
function hypot(a, b, c) {
if (arguments.length == 3) {
return Math.sqrt(a * a + b * b + c * c);
} else {
return Math.sqrt(a * a + b * b);
}
}
console.log(hypot(3, 4));
console.log(hypot(1, 4, 8));
Or we could inquire directly about c
, which takes on the magic value of undefined
if the caller doesn’t provide an actual parameter:
function hypot(a, b, c) {
if (c !== undefined) {
return Math.sqrt(a * a + b * b + c * c);
} else {
return Math.sqrt(a * a + b * b);
}
}
console.log(hypot(3, 4));
console.log(hypot(1, 4, 8));
Our other option is to try and generalize the code so that one definition serves all types. In the case of hypot
, we can make it work with just two dimensions by forcing c
to be 0. But instead of relying on the caller to do that, we provide a default parameter:
function hypot(a, b, c=0) {
return Math.sqrt(a * a + b * b + c * c);
}
console.log(hypot(3, 4));
console.log(hypot(1, 4, 8));
Neither of these approaches is overloading, but it makes us feel like overloading is supported.
Let’s try this out in pure C:
#include <math.h>
double hypot(double a, double b) {
return sqrt(a * a + b * b);
}
double hypot(double a, double b, double c) {
return sqrt(a * a + b * b + c * c);
}
We find that this code doesn’t even compile. There are some hacks to emulate it as we did in Javascript, but they go against the grain of the language.
Overloading is a nice feature to have, I think. In a sense, it ascribes an identity on a function that is a compound primary key: its name plus its parameters. Languages without overloading use a much simpler primary key: the name alone. When we get to Haskell, we’ll take this idea of overloading even further and provide different definitions of a function differentiated not just by parameter type, but by parameter value.
There we have it. Two different means of making our functions handle several different types. And neither has to do with inheritance! Next we’ll talk about subtype polymorphism.
To experiment with subtype polymorphism in C++, we’re going to create a terminal-based graphing calculator. Plotting in the terminal sounds a little awkward, and it is. But it’ll be fun. We’ll use the Ncurses library to position the cursor at arbitrary locations on the screen. Here’s a first step, which plots just the x- and y-axes.
#include <ncurses.h>
class Calculator {
public:
Calculator() {
initscr();
clear();
getmaxyx(stdscr, height, width);
}
void plot() {
int mid_x = width / 2;
int mid_y = height / 2;
for (int x = 0; x < width; ++x) {
mvprintw(mid_y, x, ".");
}
for (int y = 0; y < height; ++y) {
mvprintw(y, mid_x, ".");
}
getch();
endwin();
}
private:
int width;
int height;
};
Here’s a main
method to get it up and running. Eventually we’ll feed it some functions, but not today.
int main(int argc, char **argv) {
Calculator calc;
calc.plot();
return 0;
}
Here’s your TODO list for next time:
- Read C++ Virtual Functions. Create super- and subclasses in C++ with instance variables of your choosing. Make an instance of the subclass in a
main
. Compile and run your code in a debugger:On a quarter sheet, write down the internal layout of your object on a quarter sheet.g++ -g source.cpp # compile with debugging support gdb ./a.out (gdb) break main # set a breakpoint on main (gdb) run # execute until breakpoint (gdb) n # run to the next statement, repeat as necessary
- Start working on the Ractor homework, which is due before April 2.
See you then!
P.S. Here’s the code we wrote together:
zing.cpp
#include <iostream>
using std::ostream;
#define FOO 6
class Zing {
public:
Zing(const char *source) {
length = strlen(source);
s = new char[length];
memcpy(s, source, length);
}
char operator[](int i) const {
return s[i];
}
char &operator[](int i) {
return s[i];
}
~Zing() {
delete[] s;
}
int LeNgTh() const {
return length;
}
private:
char *s;
int length;
};
ostream &operator<<(ostream &stream, const Zing &z) {
for (int i = 0; i < z.LeNgTh(); ++i) {
stream << z[i];
}
return stream;
}
int main(int argc, char **argv) {
Zing z = "Jordan's Sister";
std::cout << "z: " << z << std::endl;
z[0] = 'G';
std::cout << "z: " << z << std::endl;
/* operator<<(operator<<(operator<<(cout, "z: "), z), endl); */
return 0;
}
hypot.js
// function hypot(a, b) {
// return Math.sqrt(a * a + b * b);
// }
function hypot(a, b, c) {
// if (c === undefined) {
if (arguments.length == 2) {
return Math.sqrt(a * a + b * b);
} else {
return Math.sqrt(a * a + b * b + c * c);
}
}
function hypot(a, b, c=0) {
var sum = 0;
arguments.forEach(arg -> sum += arg * arg);
return Math.sqrt(sum);
// return Math.sqrt(a * a + b * b + c * c);
}
console.log("hypot(3, 4):", hypot(3, 4, null)); // 5
console.log("hypot(1, 4, 8):", hypot(1, 4, 8)); // 9
Calc.cpp
#include <iostream>
#include <ncurses.h>
class Calc {
public:
Calc() {
initscr();
clear();
getmaxyx(stdscr, height, width);
}
void plot() {
getch();
endwin();
}
private:
int width;
int height;
};
int main(int argc, char **argv) {
Calc c;
c.plot();
return 0;
}