Show Lecture.Functors as a slide show.
CS253 Functors
Please pronounce carefully.
A Method
Consider this class:
class Foo {
public:
int thrice(int n) { return n*3; }
};
Foo f;
cout << f.thrice(42) << '\n';
126
int thrice(int n)
- return type:
int
- name of the method:
thrice
- argument list:
(int n)
An operator function
Let’s use the ~
operator, which is defined as bit-wise complement
for integral types:
class Foo {
public:
const char *operator~() { return "hello"; }
};
Foo f;
cout << ~f << '\n';
hello
const char *operator~()
- return type:
const char *
- name of the method:
operator~
- argument list:
()
- Not to be confused with the destructor, which is named
~Foo
.
Another operator function
Let’s use the ()
operator, alias the “function call” operator:
class Foo {
public:
double operator()(int i, float f) { return i+f; }
};
Foo f;
cout << f(3, 1.2) << '\n';
4.2
double operator()(int i, float f)
- return type:
double
- name of the method:
operator()
- argument list:
(int i, float f)
Sounds useless
😒
- Hooray!
- We’ve created an alternate way to write a function.
- It has the advantage of being more confusing.
- However, it can also have state.
Another operator function
Let’s write a functor that returns its argument+1:
class BiggerBy1 {
public:
int operator()(int i) { return i+1; }
};
BiggerBy1 b;
cout << b(17) << '\n';
18
int operator()(int i)
- return type:
int
- name of the method:
operator()
- argument list:
(int i)
Another operator function
It’s easy to change that to increment by two, instead:
class BiggerBy2 {
public:
int operator()(int i) { return i+2; }
};
BiggerBy2 b;
cout << b(17) << '\n';
19
However, it would be tedious to have to write many such functors
for all the different increment values we might want to use.
A stateful functor
Let’s give the functor a ctor argument:
class Bigger {
const int increment;
public:
Bigger(int inc) : increment(inc) { }
int operator()(int i) { return i+increment; }
};
Bigger b(4), c(12);
cout << b(17) << ' ' << b(100) << '\n'
<< b(1000) << ' ' << c(2000) << '\n';
21 104
1004 2012
The parentheses in b(4)
are very different than the
parentheses in b(17)
.
Another stateful functor
Another use of memory:
class Queue {
int previous=0;
public:
int operator()(int n) {
const auto save=previous;
previous=n;
return save;
}
};
Queue q;
cout << q(12) << '\n';
cout << q(100) << '\n';
cout << q(42) << '\n';
0
12
100
Another stateful functor
Why does this fail?
class Queue {
int previous=0;
public:
int operator()(int n) {
const auto save=previous;
previous=n;
return save;
}
};
Queue q;
cout << q(12) << '\n'
<< q(100) << '\n'
<< q(42) << '\n';
0
12
100
Even Queueier!
Multi-element queue:
class Queue {
deque<int> store;
public:
Queue(int init_size) : store(init_size) { }
int operator()(int n) {
store.push_back(n);
const auto f = store.front();
store.pop_front();
return f;
}
};
Queue q(3);
for (int i=10; i<20; i++)
cout << q(i) << '\n';
0
0
0
10
11
12
13
14
15
16
Or …
class Total {
int sum=0;
public:
int operator()(int n) {
return sum += n;
}
};
Total t;
for (int i=0; i<=10; i++)
cout << "Add " << i << " yielding " << t(i) << '\n';
Add 0 yielding 0
Add 1 yielding 1
Add 2 yielding 3
Add 3 yielding 6
Add 4 yielding 10
Add 5 yielding 15
Add 6 yielding 21
Add 7 yielding 28
Add 8 yielding 36
Add 9 yielding 45
Add 10 yielding 55
Sorting with Functors
- How does a container like
set
know the sorting order?
- Sure, it’s easy to answer for
int
and string
, but what
about class Student
?
- Sort by major?
- Sort by eId?
- Sort by last name, then first name, then CSUID?
- Besides, even for
int
, you might want to sort in a different order.
- You might even want multi-key sorting, like a phone book:
- primary key: last name
- secondary key: first name
- tertiary key: middle name
- quaternary key: street name
- quinary key: house number
The Idiot Savant
- Imagine that you had a semi-brilliant assistant who was very good at sorting.
- However, your assistant doesn’t know alphabetical order.
- He can do everything that’s needed for sorting except actually
determine if, say, “Jay” comes before “David”.
- Therefore, he frequently comes to you,
asking if this word comes before this other word.
set
is like that. It knows the mechanics of sorting,
but it doesn’t know what order to use. You need to help it.
Comparison functor
- You provide
set
with a comparison functor.
- The
set
code makes comparisons using the functor.
- The functor only compares two items at a time.
- I say again, two items at a time. Only two.
The functor compares—it doesn’t sort.
- The functor returns
true
iff the first item should come
before the second item.
- Why can’t you use a comparison function, as opposed to
a comparison functor?
Because the set
template takes types, not objects.
Implicit/explicit
Implicit sorting criterion:
set<int> s = {31, 41, 59, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
23 26 31 41 53 58 59 84 93 97
Explicit sorting criterion:
set<int, less<int> > s = {31, 41, 59, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
23 26 31 41 53 58 59 84 93 97
What’s this > >
folderol?
Reversed
Use greater<int>
to sort from biggest to smallest:
set<int, greater<int>> s = {31, 41, 59, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
97 93 84 59 58 53 41 31 26 23
Explicitly explicit
Use a comparison functor that behaves the same as less<int>
:
struct compare { // 😱 😱 struct‽ 😱 😱
bool operator()(int a, int b) const {
return a < b;
}
};
set<int, compare> s = {31, 41, 59, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
23 26 31 41 53 58 59 84 93 97
Two-level
Use a comparison functor that does a two-level comparison,
like last name/first name.
struct compare {
bool operator()(int a, int b) const {
int da = abs(a-50), db = abs(b-50);
if (da != db)
return da < db; // Primary sort: distance from 50
return a < b; // Secondary sort: value of number
}
};
set<int, compare> s = {31, 41, 59, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
53 58 41 59 31 26 23 84 93 97
Ternary operator
Another way to write a two-level comparison functor:
struct compare {
bool operator()(int a, int b) const {
int da = abs(a-50), db = abs(b-50);
return (da != db) ? da < db : a < b;
}
};
set<int, compare> s = {31, 41, 59, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
53 58 41 59 31 26 23 84 93 97
Curious criteria
This sorts by different criteria:
struct compare {
bool operator()(int a, int b) const {
int last_a = a%10, last_b = b%10;
if (last_a != last_b)
return last_a < last_b; // Primary sort: by last digit
return a < b; // Secondary sort: value of number
}
};
set<int, compare> s = {31, 41, 59, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
31 41 23 53 93 84 26 97 58 59
Multi-level
struct student {
int id; string name; float gpa;
};
struct compare {
bool operator()(const student &a, const student &b) const {
return (a.gpa != b.gpa) ? a.gpa > b.gpa : a.id < b.id;
}
};
set<student,compare> s = {
{81234567, "Jack Applin", 3.9},
{82828282, "Craig Partridge", 0.5},
{84444444, "Homecoming Queen", 2.2},
{83333333, "Joe Kolledge", 2.2},
};
for (const auto &v : s)
cout << v.id << ' ' << v.gpa << ' ' << v.name << '\n';
81234567 3.9 Jack Applin
83333333 2.2 Joe Kolledge
84444444 2.2 Homecoming Queen
82828282 0.5 Craig Partridge
Not all
- Of course, not all collections require a comparison functor.
vector
, array
, and list
just go in the order you specify,
and so have no comparison functor.
unordered_set
needs only an equality comparison (and a hash
functor), so its comparison functor defaults to equal_to
,
not less
.
Example #1
We’re going to build up to using functors and λ-expressions.
char embiggen(char c) {
if ('a' <= c && c <= 'z')
return c - 'a' + 'A';
else
return c;
}
int main() {
string name = "Beverly Hills Chihuahua";
for (char &c : name) // & for reference
c = embiggen(c);
cout << name << '\n';
}
BEVERLY HILLS CHIHUAHUA
Example #2
Use a ternary expression, instead:
char embiggen(char c) {
return ('a'<=c && c<='z') ? c-'a'+'A' : c;
}
int main() {
string name = "Beverly Hills Chihuahua";
for (char &c : name)
c = embiggen(c);
cout << name << '\n';
}
BEVERLY HILLS CHIHUAHUA
Example #3
Use the transform
algorithm, rather than an explicit loop:
char embiggen(char c) {
return ('a'<=c && c<='z') ? c-'a'+'A' : c;
}
int main() {
string name = "Beverly Hills Chihuahua";
string result = name; // Why?
transform(name.begin(), name.end(),
result.begin(), embiggen);
cout << result << '\n';
}
BEVERLY HILLS CHIHUAHUA
Example #4
This example uses the same buffer for input & output of transform
.
char embiggen(char c) {
return ('a'<=c && c<='z') ? c-'a'+'A' : c;
}
int main() {
string name = "Beverly Hills Chihuahua";
transform(name.begin(), name.end(),
name.begin(), embiggen);
cout << name << '\n';
}
BEVERLY HILLS CHIHUAHUA
Example #5
This code uses an actual functor, as opposed to a function.
class embiggen {
public:
char operator()(char c) {
return ('a'<=c && c<='z') ? c-'a'+'A' : c;
}
};
string name = "Beverly Hills Chihuahua";
embiggen biggifier;
transform(name.begin(), name.end(),
name.begin(), biggifier);
cout << name << '\n';
BEVERLY HILLS CHIHUAHUA
Example #6
Create a temporary functor object:
class embiggen {
public:
char operator()(char c) {
return ('a'<=c && c<='z') ? c-'a'+'A' : c;
}
};
string name = "Beverly Hills Chihuahua";
transform(name.begin(), name.end(),
name.begin(), embiggen());
cout << name << '\n';
BEVERLY HILLS CHIHUAHUA
Example #7
Instead of a functor, a lambda-expression can also be used.
string name = "Beverly Hills Chihuahua";
transform(name.begin(), name.end(),
name.begin(),
[](char c){ return 'a'<=c && c<='z' ? c-'a'+'A' : c; }
);
cout << name << '\n';
BEVERLY HILLS CHIHUAHUA
Example #8
Don’t re-invent the wheel:
string name = "Beverly Hills Chihuahua";
for (char &c : name)
c = toupper(c);
cout << name << '\n';
BEVERLY HILLS CHIHUAHUA