Show Lecture.Functions as a slide show.
CS253 Functions
Definition
Remember that a method is just a function associated with a class.
Everything said here applies equally well to methods,
because methods are functions.
Functions
By & large, C++ functions look like Java functions:
int sum(int a, int b) {
return a+b;
}
int main() {
cout << sum(2, 40) << '\n';
}
42
Default Parameters
The trailing arguments can have default values:
int sum(int a, int b = 13) {
return a+b;
}
int main() {
cout << sum(2, 40) << '\n'
<< sum(8) << '\n';
}
42
21
Are there two functions called sum
, or just one?
There’s only one, which always takes two arguments. If you call sum
with a single argument, the compiler adds a second argument of 13
.
Default Parameters
All default arguments must be piled up at the end:
int sum(int a = 123, int b = 456) {
return a+b;
}
int main() {
cout << sum(2, 40) << '\n'
<< sum(8) << '\n'
<< sum() << '\n';
}
42
464
579
int bad(int c=8, long d) { // 🦡
return c+d;
}
int main() {
return bad(5);
}
c.cc:1: error: default argument missing for parameter 2 of ‘int bad(int, long
int)’
Function Overloading
Like Java, C++ can have multiple versions
of the same function that take different arguments:
int sum(int a, int b) { return a+b; }
int sum(int a) { return a+456; }
int sum() { return 123+456; }
int main() {
cout << sum(2, 40) << '\n'
<< sum(8) << '\n'
<< sum() << '\n';
}
42
464
579
Are there three functions called sum
, or just one?
Three. They’re right up there. ⇑
Function Overloading
The types don’t have to be the same, either:
int sum(int a, int b) { return a+b; }
double sum(int a) { return a+4.5; }
string sum(double) { return "I can’t deal with double"; }
const char *sum() { return "You are a bonehead"; }
int main() {
cout << sum(2, 40) << '\n'
<< sum(8) << '\n'
<< sum(123.456) << '\n'
<< sum() << '\n';
}
42
12.5
I can’t deal with double
You are a bonehead
For sum(double)
, the unused argument isn’t given a name,
to avoid warnings about unused variables.
Implementation
- So, how is this accomplished?
- How can there actually be four versions of the function
sum
in the executable file?
sum
is sum
, right?
- No.
Name Mangling
- It’s called name mangling.
- Inside the executable file, the function isn’t really called
sum
.
- Instead, it’s called
sum-with-an-int-argument
.
- Not really—it’s more compact than that.
Return type doesn’t matter
- The function’s return type is not part of the mangled name.
- Therefore, a function can’t be overloaded on return type alone.
int foo() { return 1; }
double foo() { return 2.0; } // 🦡
int main() {
return 0;
}
c.cc:2: error: ambiguating new declaration of ‘double foo()’
Name Mangling
$ cat ~cs253/Example/overload.cc
int sum(int a, int b) { return a+b; }
double sum(int a) { return a+4.5; }
const char *sum() { return "You are a bonehead"; }
bool sum(double, int, short) { return false; }
$ g++ -Wall -c ~cs253/Example/overload.cc
$ nm --numeric-sort overload.o 2>&-
0000000000000000 T _Z3sumii
0000000000000014 T _Z3sumi
000000000000002e T _Z3sumv
0000000000000039 T _Z3sumdis
$ nm --numeric-sort --demangle overload.o 2>&-
0000000000000000 T sum(int, int)
0000000000000014 T sum(int)
000000000000002e T sum()
0000000000000039 T sum(double, int, short)
More Name Mangling
$ cat ~cs253/Example/mangle.cc
void b(unsigned __int128,
__int128,
long double,
unsigned char,
long double,
signed char,
double) {
}
$ g++ -Wall -c ~cs253/Example/mangle.cc
$ nm mangle.o 2>&-
0000000000000000 T _Z1bonehead
Do not memorize the various encodings for types.
It varies between compilers, anyway.
// The “Golden Ratio”:
auto φ() {
return (1+sqrt(5.0L))/2;
}
auto fact() {
cout << "φ ≈ " << φ() << '\n';
}
int main() {
fact();
return 0;
}
φ ≈ 1.61803
The return type can be declared as auto, which means that the actual type
will be deduced by what is returned. auto is not a type.
It means “Compiler, you figure out the real type.”
What is the return type of fact
?
void. It has no return statement.
It’s certainly not auto, because auto is not a type,
any more than your mother’s name is “Mom”.
What is the return type of φ
?
The type of 5.0L
is long double, so sqrt(5.0L)
is
long double, so 1+sqrt(5.0L)
is long double, so
(1+sqrt(5.0L))/2
is long double, so … long double.
Caution
The auto type deduction tolerates no ambiguity.
auto age_of_earth(bool science) {
if (science)
return 4.54e9; // science: 4.54 billion years ago
else
return 4004-1+2024; // Archbishop Ussher: 4004BC 🦡
}
int main() {
cout << age_of_earth(true);
}
c.cc:5: error: inconsistent deduction for auto return type: ‘double’ and
then ‘int’
Pass by value
By default, arguments are passed by value.
void foo(int n) {
++n; ++n;
}
int main() {
int v = 42;
foo(v);
cout << v << '\n';
}
42
The variable n
is a copy of v
.
Pass by address
A C programmer might pass by address:
void foo(int *n) {
*n = 99;
}
int main() {
int v = 42;
foo(&v);
cout << v << '\n';
}
99
The actual parameter is &v
, the address of v
. This is fine C,
but C++ can do better. Use a reference, instead.
Pass by reference
C++ can pass a variable by reference:
void foo(int &n) {
++n; ++n;
}
int main() {
int v = 42;
foo(v);
cout << v << '\n';
}
44
The variable n
is an alias of v
. This is often implemented
as a pointer that gets *
applied automatically.
Pass by constant address
You can use a read-only pointer:
void foo(const int *n) {
*n = 99; // 🦡
}
int main() {
int v = 42;
foo(&v);
cout << v << '\n';
}
c.cc:2: error: assignment of read-only location ‘* n’
As before, avoid pointer arguments in favor of reference arguments.
Pass by reference
You can use a read-only reference:
void foo(const int &n) {
++n; ++n; // 🦡
}
int main() {
int v = 42;
foo(v);
cout << v << '\n';
}
c.cc:2: error: increment of read-only reference ‘n’
Efficiency
- Let’s talk about how arguments get passed to a function,
on today’s computers.
- When passing an integer, a floating-point value, or a boolean, pass by
value if you can, because that will go into a lightning-fast
register. Copying a small scalar into a register is cheap.
It you’re lucky, it already lives in a register, not the stack.
- An object, however, is generally bigger, and might even contain
pointers to dynamic data (vector, string).
- Therefore, passing an object by value tends to be expensive,
whereas passing an object by address or reference is cheap.
- It’s all about making a copy. Is that fast, for your datatype?
Efficiency of pass by value
bool foo(vector<int> v) { // 🦡 pass by value
return v.empty();
}
int main() {
vector<int> w(2500);
for (int i=0; i<10'000'000; i++)
foo(w);
cout << "Done.\n";
}
Done.
Real time: 1.99 seconds
We made ten million copies of a ten-thousand-byte vector,
and so copied 100 gigabytes. Slow! 🐌
Efficiency of pass by constant reference
bool foo(const vector<int> &v) { // pass by constant reference
return v.empty();
}
int main() {
vector<int> w(2500);
for (int i=0; i<10'000'000; i++)
foo(w);
cout << "Done.\n";
}
Done.
Real time: 5.53 ms
No copies were made! 😎
Rule of thumb
- When sending data to a function:
- When the function is supposed to change the argument:
- Pass by non-constant reference.
- I/O streams always get passed this way.
double sqrt(double d);
int count_spaces(const string &s);
void get_physical_info(int &height, int &weight);
void remove_spaces(string &s);
void read_picture(Picture &pic, ostream &os);
void write_picture(const Picture &pic, ostream &os);
Arrays
Arrays are special. They’re automatically passed by reference:
void foo(int a[]) {
a[0] = 911;
}
int main() {
int numbers[] = {555-1212, 867-5309}; // dashes⁇
foo(numbers);
cout << numbers[0];
}
911
This only applies to old-style C arrays,
not std::vector or std::array.