Show Lecture.Functors as a slide show.
CS253 Functors
Please pronounce carefully.
A Method
once:
💉
twice: 💉💉
thrice: 💉💉💉
Consider this class:
class Foo {
public:
int thrice (int n) const { return n*3; }
};
Foo f;
cout << f.thrice(42) << '\n';
126
int thrice (int n) const
- return type: int
- name of the method:
thrice
- argument list:
(int n)
- const: it doesn’t change any of the no data in
class Foo
An operator function
Let’s use the unary ~
operator, defined as bit-wise complement
for integers.
class Foo {
public:
const char * operator~ () const { return "hello"; }
};
Foo f;
cout << ~f << '\n';
hello
const char * operator~ () const
- return type: const char *
- name of the method:
operator~
- argument list:
()
- const: it doesn’t change any object data
- Not to be confused with the destructor, which is named
~Foo
.
Another operator function
Let’s use the unary postfix ()
operator, alias the “function call”
operator:
class Foo {
public:
double operator() (int i, float f) const { return i+f; }
};
Foo f;
cout << f(3, 1.2) << '\n';
4.2
double operator() (int i, float f) const
- return type: double
- name of the method:
operator()
- argument list:
(int i, float f)
- const: it doesn’t change any object data
f
is not a function! f
is a Foo
object!
It sure behaves as a function.
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) const { return i+1; }
};
BiggerBy1 b;
cout << b(17) << '\n';
18
int operator()(int i) const
- return type: int
- name of the method:
operator()
- argument list:
(int i)
- const: it doesn’t change any object data
Another operator function
It’s easy to change that to increment by two, instead:
class BiggerBy2 {
public:
int operator()(int i) const { 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) const { return i+increment; }
};
Bigger b(4), c(12);
cout << b(17) << ' ' << b(100) << '\n'
<< b(1000) << ' ' << c(2000) << '\n';
21 104
1004 2012
- The parens in the ctor,
b(4)
, are very different
than the parens in the expression b(17)
.
b(4)
constructs an object of type Bigger
, named b
.
b(17)
is b.operator()(17)
.
- How do I tell if it’s calling the ctor or
operator()
?
- Well, are you constructing the object?
Is this its creation ?
ID numbers
The Local Static lecture generated ID numbers using a
local static variable. Now, with a functor:
class ID {
long id;
const short inc;
public:
ID(long id=800'00'0000, short inc=13) : id(id), inc(inc) { }
auto operator()() { return id += inc; } // NOT const
};
int main() {
ID student_id; // NOT student_id()
ID employee_id(10000,1);
cout << student_id() << ' ' << student_id() << '\n'
<< employee_id() << ' ' << employee_id() << '\n';
}
800000013 800000026
10001 10002
Another stateful functor
Another use of memory. You recall that NAN means Not a Number.
No const, because operator()
changes previous
.
class Queue {
double previous = NAN;
public:
double operator()(double d) {
const auto save = previous;
previous = d;
return save;
}
};
Queue q;
cout << q(1.2) << '\n';
cout << q(9.9) << '\n';
cout << q(3.4) << '\n';
nan
1.2
9.9
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 << " sum " << t(i) << '\n';
Add 0 sum 0
Add 1 sum 1
Add 2 sum 3
Add 3 sum 6
Add 4 sum 10
Add 5 sum 15
Add 6 sum 21
Add 7 sum 28
Add 8 sum 36
Add 9 sum 45
Add 10 sum 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 🞬🞬🞬🞬🞬 🞬🞬🞬🞬🞬🞬
- 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 argument
should be before the second argument in the set. That’s all.
Why can’t set use a comparison function, as opposed to a comparison functor ?
Because the set template takes types, not objects or
values.
Implicit/explicit
Implicit sorting criterion:
set<int> s = {31, 49, 41, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
23 26 31 41 49 53 58 84 93 97
Explicit sorting criterion:
set<int, less<int> > s = {31, 49, 41, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
23 26 31 41 49 53 58 84 93 97
What’s this > >
business?
An old problem with the >>
token. Compilers used to have
problems regarding >>
as the shift-right operator, as opposed to
two closing angle brackets. It’s no longer needed, but you still
see it in old code, or new code written by old coders 🧓.
Reversed
Use greater<int>
to sort from biggest to smallest:
set<int, greater<int>> s = {31, 49, 41, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
97 93 84 58 53 49 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, 49, 41, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
23 26 31 41 49 53 58 84 93 97
Make sure that the comparison functor is const. Compilers are getting
fussier about this for comparison functors used with sort()
or STL containers.
Two-level
Use a comparison functor that does a two-level comparison;
primarily by proximity to 45, secondarily by value.
struct compare {
bool operator()(int a, int b) const {
int da = abs(a-45), db = abs(b-45); // distance from 45
if (da != db)
return da < db; // Primary sort: distance from 45
return a < b; // Secondary sort: value of number
}
};
set<int, compare> s = {31, 49, 41, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
41 49 53 58 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-45), db = abs(b-45); // distance from 45
return (da != db) ? da < db : a < b;
}
};
set<int, compare> s = {31, 49, 41, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
41 49 53 58 31 26 23 84 93 97
Let pair::operator<
do the work.
A cool technique: offload the multi-level comparison to pair,
which naturally compares that way:
struct compare {
bool operator()(int a, int b) const {
pair pa = {abs(a-45), a}, // Love that CTAD!
pb = {abs(b-45), b};
return pa < pb;
}
};
set<int, compare> s = {31, 49, 41, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
41 49 53 58 31 26 23 84 93 97
“But that’s less efficient.”
“Did you measure it, or are you guessing?”
Let pair::operator<
do the work.
Or, eliminating the pair variables that are only used once:
struct compare {
bool operator()(int a, int b) const {
return pair{abs(a-45), a} < pair{abs(b-45), b};
}
};
set<int, compare> s = {31, 49, 41, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
41 49 53 58 31 26 23 84 93 97
Curious criteria
This sorts first by final digit, then by value:
struct compare {
bool operator()(int a, int b) const {
int last_a = a%10, last_b = b%10; // Final digits
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, 49, 41, 26, 53, 58, 97, 93, 23, 84};
for (auto v : s)
cout << v << ' ';
31 41 23 53 93 84 26 97 58 49
Multi-level
struct student {
int id; float gpa; string name;
};
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, 3.9, "Jack Applin"},
{82828282, 0.5, "Craig Partridge"},
{84444444, 2.2, "Homecoming Queen"},
{83333333, 2.2, "Joe Kolledge"},
};
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') // 'a' <= c <= 'z' will NOT work
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;
transform(name.begin(), name.end(),
result.begin(), embiggen);
cout << result << '\n';
}
BEVERLY HILLS CHIHUAHUA
Why assign name
to result
?
Can’t write to a string with no space allocated.
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) const {
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) const {
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
Sprechen Sie Deutsch?
Warning notice on the traffic light system of the Dresden Roads and Civil Engineering Office
The previous eight examples assume:
- Each lowercase letter fits in a char.
- Each uppercase letter is also one byte.
- Each lowercase letter has an uppercase equivalent.
However:
- Eszett, ß, U+00DF, an sz
ligature, is two bytes in UTF-8.
- The capital letter, ẞ, U+1E9E, is three bytes in UTF-8
(because it came later?).
- Pre-2017, ẞ was unofficial in Germany, so
“Straße”
became “STRASSE” in uppercase. Post-2017, “STRAẞE”
is also acceptable.