Advanced Software Engineering with C++ Templates Templates II: - - PowerPoint PPT Presentation

advanced software engineering with c templates
SMART_READER_LITE
LIVE PREVIEW

Advanced Software Engineering with C++ Templates Templates II: - - PowerPoint PPT Presentation

Advanced Software Engineering with C++ Templates Templates II: Traits Thomas Gschwind <thg at zurich dot ibm dot com> Templates II: Traits Specialization Traits Default Values Th. Gschwind. Advanced Software Engineering with C++


slide-1
SLIDE 1

Advanced Software Engineering with C++ Templates

Templates II: Traits Thomas Gschwind <thgatzurichdotibmdotcom>

slide-2
SLIDE 2

Templates II: Traits

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

182

§ Specialization § Traits § Default Values

slide-3
SLIDE 3

pvector<T>

§ Implement the persistent vector data type. § Experiment with the persistent vector and use it in combination with different data types. What do you observe? Why do you

  • bserve that behavior? How can it be changed?

§ What is an interesting data type for T? § Why is it interesting?

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

183

slide-4
SLIDE 4

A Persistent Vector Class (cont’d)

template<typename T> class pvector { string filename; vector<T> v; void readvector() { ifstream ifs(filename); for (;;) { T x; ifs >> x; if (!ifs.good()) break; v.push_back(x); } } void writevector() {

  • fstream ofs(filename);

typename vector<T>::iterator fst=v.begin(), lst=v.end(); while (fst!=lst) ofs << *fst++ << endl; } …

What if we use string as type parameter? Reads a string up to the next whitespace Writes a string with and without whitespace

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

184

slide-5
SLIDE 5

pvector for string?

§ Use template specialization § Partial specialization allows us to change the implementation of a template for a specific class § Very easy to implement but very repetitive

template<> class pvector<string> { string filename; vector<string> v; void readvector() { … } void writevector() { … } … // repeat all the other methods as is }

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

185

slide-6
SLIDE 6

Sidebar: Inheritance

§ The following shows a sample child class inheriting from a sample base class

  • Typically, we use “: public” to indicate “extends”
  • Typically, functions to be overridden in the child are declared virtual
  • We will come back to the gory details (do not use inheritance yet)
  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

186

struct base { virtual void print() const { cout << "base" << endl; } }; struct child : public base { virtual void print() const { cout << "child" << endl; } }; void test(const base &b) { b.print(); }

For inheritance to work as “expected” we typically use pointers or references.

slide-7
SLIDE 7

§ Use inheritance

  • Need to change readvector and writevector in parent class to be virtual
  • Implied dynamic dispatch although unnecessary
  • We need a new class with a new name pvectorstring?
  • Of course, we can fix the name problem

template<typename T> class pvector : public pvector_base<T> { using pvector_base<T>::pvector_base; // inherit constructors

};

template<> class pvector<string> : public pvector_base<string> {

using pvector_base<string>::pvector_base;

// TODO: specialize readvector, writevector };

class pvectorstring : public pvector<string> {

virtual void readvector() { … } virtual void writevector() { … } };

pvector for string? (cont’d)

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

187

Not bad, but we can do better than that

slide-8
SLIDE 8

pvector for string? (cont’d)

§ Factor out the persistence logic into a separate interface

  • Need to pass the vector to be read/written as extra parameter
  • Yes, not so repetitive
  • Yes, the persistence logic can be reused

template<typename T> struct pvector_serialize { virtual void readvector(string fn, vector<T> &v) = 0; virtual void writevector(string fn, const vector<T> &v) = 0; };

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

188

slide-9
SLIDE 9

pvector with inheritance based serializer

§ Pass serializer to pvector in constructor

template<typename T> class pvector { string filename; vector<T> v; pvector_serializer<T> *serializer; public: pvector(string fname, pvector_serializer<T> *ser) : filename(fname), serializer(ser) { serializer->readvector(fname, v); } ~pvector() { serializer->writevector(fname, v); } … };

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

189

slide-10
SLIDE 10

pvector for string? (cont’d)

§ Current solution is rather coarse grained § Would be better to just factor out the read and write function § Gives better reuse (left as exercise) § Trouble is we always use virtual method calls although

  • We know the type of T
  • Moreover, we typically know the serializer to be used upfront

template<typename T> struct element_serializer { virtual void read(ifstream &i, T &elem) { … }; virtual void write(ofstream &o, const T &elem) { … }; };

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

190

slide-11
SLIDE 11

Templates: Trait Classes

§ Templates can also be used to provide polymorphism

  • Same functionality, same everything, just more efficient

§ Inheritance allows to define an interface to be provided by subtypes § Templates allow us to define an interface to be used (traits) § With inheritance subtypes may override members § The traits class is specialized for different types (override members)

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

191

template<typename T> struct persistence_traits { static void read(ifstream &i, T &elem) {…} static void write(ofstream &o, const T &elem) {…} } template<> struct persistence_traits<string> { static void read(ifstream &i, string &elem) {…} static void write(ofstream &o, const string &elem) {…} } Same principle as for min<char*> Default implementation

slide-12
SLIDE 12

pvector: Refactoring

§ First, let’s refactor the pvector class to move the read and write methods into external classes…

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

192

slide-13
SLIDE 13

pvector: Using the Trait class

template<typename T> struct persistence_traits { static void read(ifstream &i, T &elem) {…} static void write(ofstream &o, const T &elem) {…} } template<> struct persistence_traits<string> { static void read(ifstream &i, string &elem) {…} static void write(ofstream &o, const string &elem) {…} } template<typename T> class pvector { void writevector() {

  • fstream ofs(filename);

vector<T>::iterator fst=v.begin(), lst=v.end(); while (fst!=lst) persistence_traits<T>::write(ofs, *fst++); } Since T is known at compile- time, this function is known at compile-time

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

193

slide-14
SLIDE 14

pvector for string? (cont’d)

§ Factor out the persistence logic into a separate class

  • Yes, not so repetitive
  • Yes, the persistence logic can be reused
  • Yes, everything determined at compile time

§ But what if we want to use different persisters?

  • Now, let’s use our traits class in a polymorphic way
  • Sort of like we are used from object-oriented programming
  • Except that types are resolved during compile time
  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

194

slide-15
SLIDE 15

Traits are similar to Inheritance except …

§ Inheritance allows us to use different implementations § Similarly, allow to specify the use of different implementations, independently of the trait’s type § … except all type inference is known, verified, and resolved during compile time

template<typename T, typename P> class pvector { void writevector() {

  • fstream ofs(filename);

vector<T>::iterator fst=v.begin(), lst=v.end(); while(fst!=lst) P::write(ofs, *fst++); } } pvector<string, persistence_traits<string> > avector;

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

195

slide-16
SLIDE 16

pvector for string? (cont’d)

§ Make the persistence class generic

  • Yes, now we can even specify the persister
  • Yes, the code is as readable as generic Java code
  • Yes, BUT creating a new object becomes tedious
  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

196

slide-17
SLIDE 17

Templates: Default Values

§ Always having to specify the persistence trait is tedious § Like with function parameters, C++ allows to assign a default value to template parameters § Now, we can only complain about certain style issues

template<typename T, typename P > class pvector { void writevector() {

  • fstream ofs(filename);

vector<T>::iterator fst=v.begin(), lst=v.end(); while(fst!=lst) P::write(ofs,*fst++); } } pvector<string, persistence_traits<string> > avector; template<typename T, typename P=persistence_traits<T> > Default traits class =============================

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

197

slide-18
SLIDE 18

pvector: Beauty Contest

§ Typically, as part of a parameterized class, we include certain typedefs for convenience and readability § We will come back to the usefulness when we talk about binders

template<typename T, typename P=persistence_traits<T> > class pvector { … public: typedef P persister; typedef typename vector<T>::iterator iterator; … void writevector() {

  • fstream ofs(filename);

iterator fst=v.begin(), lst=v.end(); while(fst!=lst) persister::write(ofs,*fst++); } } As discussed, typename helps the compiler to identify that the following is a typename

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

198

slide-19
SLIDE 19

Traits: Summary

Traits allow us to use an aspects of a type in a polymorphic way § The aspect is implemented by a generic template class § The aspect’s implementation can be overridden for certain types T (with template specialization) § Since T is known during compile-time, the functions to be invoked will also be known at compile time § Allows the compiler to inline those functions and to perform more

  • ptimizations (for instance, if certain parameters are constants)

§ The trait class can be used by any other class (template or not) § Using an additional template parameter, we can use different trait classes in our template

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

199

slide-20
SLIDE 20

BREAK?

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

200

slide-21
SLIDE 21

Advanced Software Engineering with C++ Templates

Standard Library: Algorithms, Functors, Design Thomas Gschwind <thgatzurichdotibmdotcom>

slide-22
SLIDE 22

Standard Library Algorithms

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

202

§ Design Principles § Iterators § Algorithms § Function Operators

slide-23
SLIDE 23

Design of C++ Algorithms

§ Containers alone are useful but not so useful

  • Typically we search for elements, remove them, iterate over them, …

§ Examples

  • find(…, x)
  • Locates x in a sequence/container
  • Returns an iterator/pointer to that element
  • find_if(…, pred)
  • Similar to find, but iterator/pointer to first elemet fulfilling pred

§ How shall we solve it?

  • Naive implementation of find()
  • Generalizing the find() function
  • Extend it to find_if()
  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

203

slide-24
SLIDE 24

Design: find (V0.1)

§ C++ is based on C, and tries to be close to C § We start out by reusing pointers for algorithms § Problem 1: Can only be used for int arrays § Problem 2: Assumes knowledge about the implementation (int*)

int *find(int *array, int n, int x) { int *p=array; for (int i=0; i<n; i++) { int val=*p; if (val==x) return p; p++; } return NULL; }

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

204

slide-25
SLIDE 25

Design: find (V0.2)

§ Addressing 1: replace the int* with a template pointer § Problem 2: Still assumes knowledge about the implementation (T*)

template<typename T> T *find(T *array, int n, const T &x) { for (int i=0; i<n; i++) { T val=*array; if (val==x) return array; array++; } return NULL; }

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

205

slide-26
SLIDE 26

Iterators (to be continued)

§ Work like a pointer § Implement the following operations (at least)

  • T operator*();
  • Ptr &operator++(); // ++prefix
  • const Ptr &operator++(int); // postfix++
  • bool operator==(const Ptr &) const;
  • bool operator!=(const Ptr &) const;

§ Note: std::iterator is a convenience class that provides a set

  • f typedefs
  • iterator_category, value_type,

difference_type, pointer, and reference

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

207

slide-27
SLIDE 27

Design: find (V0.3)

§ Addressing 2: We do not know the exact type of iterator § Addressing 2: Iterator becomes another template parameter § Problem 3: We may not always know the number of elements (iterate over list, iterate over partial set, etc.) § Problem 4: Return value if the element is not found

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

208

template<typename Iter, typename T> Iter find(Iter begin, int n, const T &x) { for (int i = 0; i < n; i++) { T val = *begin; if (val == x) return begin; begin++; } return 0; // compiler error } Here the iterator’s value type is converted to a value of type T which may not be

  • desired. Hence, it is better to write:

auto val=*begin; // C++11, or typename Iter::value_type val=*begin;

slide-28
SLIDE 28

Design: find (V0.6)

§ Addressing 3: Number of elements? Pass in an end iterator. § Addressing 4: Exception? Makes the algorithm clumsy to use. § Addressing 4: Special NULL iterator? Recycle end as NULL iterator by looking for x in [begin, beyond) § Problem 5: “Efficiency”

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

209

template<typename Iter, typename T> Iter find(Iter begin, Iter beyond, const T &x) { for (; begin<beyond; begin++) { if (*begin==x) return begin; } return beyond; }

slide-29
SLIDE 29

Iterators: Postfix versus Prefix Increment Operator

§ Postfix var++ returns the old value and increments var § Prefix ++var, increments var and returns its value § Hence postfix ++/-- copy the iterator and are less efficient § Prefer prefix ++/-- operator over the postfix operator

T &Iterator2::operator++() { // prefix this->pos++; return *this; } T Iterator2::operator++(int) { // postfix T t(*this); this->pos++; return t; }

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

211

slide-30
SLIDE 30

Design: find (V0.8)

§ Addressing 5: Use prefix ++ operator § Can be used for any container, any data type, any iterator, and works well! :-) § Problem 6: Style (as always)

template<typename Iter, typename T> Iter find(Iter begin, Iter beyond, const T &x) { for (; begin<beyond; ++begin) { if (*begin==x) return begin; } return beyond; }

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

212

begin==beyond at this point. We can use this to shorten the algorithm further.

slide-31
SLIDE 31

Design: find (V1.0)

§ Perfectly optimal and generic function § It doesn‘t get better than that § Any questions?

template<typename Iter, typename T> Iter find(Iter begin, Iter beyond, const T &x) { while ((begin!=beyond) && (*begin!=x)) ++begin; return begin; }

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

213

slide-32
SLIDE 32

Design: find_if (V0.2)

§ Find an element that meets a given condition § Create an interface and pass object implementing that interface

  • No, we do not always want to create a new interface
  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

214

slide-33
SLIDE 33

Design: find_if (V0.4)

§ Allow user to pass a function (pred) that evaluates the condition

  • Functions are stored in memory
  • Hence, we can take the address of the function and

use a pointer of the type of the function

  • A pointer to a function taking an int as parameter and returning bool

can be declared as bool (*pred)(int)

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

215

template<typename Iter> Iter find_if(Iter begin, Iter beyond, bool (*pred)(typename Iter::value_type)) { while (begin != beyond) { if (pred(*begin)) break; ++begin; } return begin; }

slide-34
SLIDE 34

Design: find_if (V0.4)

§ Need to always be a function, looks complicated § Function always called through a function pointer § Template parameters are not limited to just classes

  • They can be a type, a function, or even concrete values
  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

217

template<typename Iter> Iter find_if(Iter begin, Iter beyond, bool (*pred)(typename Iter::value_type)) { while (begin != beyond) { if (pred(*begin)) break; ++begin; } return begin; }

slide-35
SLIDE 35

Design: find_if (V1.0)

§ Now, pred may be a

  • Function (as before)
  • Lambda expression (again as before), or a
  • Function object (we will soon come to this)

template<typename Iter, typename Pred> Iter find_if(Iter begin, Iter beyond, Pred pred) { while (begin!=beyond) { if (pred(*begin)) break; ++begin; } return begin; }

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

218

slide-36
SLIDE 36

Algorithms (#include <algorithm>)

§ Non mutating

  • for_each, find, adjacent_find, count, mismatch, …

§ Mutating

  • copy, swap, transform, replace, fill, unique, …

§ Sorting

  • sort, nth_element, binary_search, partition, …

§ Sets

  • includes, set_union, set_intersect, set_difference, …

Decide which procedures you want; use the best algorithms you can find. Bjarne Stroustrup

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

219

slide-37
SLIDE 37

Algorithms: for_each

§ Calls f for each element in [first, last) § O(n) § Attention!

  • There is a controversy whether this algorithm may change its elements
  • It is listed as non-mutable algorithm
  • Arguments have been made whether this applies only to the element order
  • r the elements (first to last) itself
  • It is not enforced by the compiler

template <typename In, typename Op> Op for_each(In first, In last, Op f);

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

220

slide-38
SLIDE 38

Algorithms: Output with for_each

§ f can be a unary function

template <typename T> void my_print(const T &x) { cout << x << endl; } int main(int argc, char *argv[]) { // … for_each(v1.begin(), v1.end(), my_print); } template <typename In, typename Op> Op for_each(In first, In last, Op f);

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

221

slide-39
SLIDE 39

Lambda Functions (C++11)

§ [] captures the environment that visible within the lambda function

  • [&cnt, …] would pass a reference to the local variable cnt
  • [cnt, …] indicates a value
  • [&] and [=] allow to indicate the entire current scope
  • Careful, [=] can generate a lot of copies

§ Compiler generates a function object for each lambda function § Prior to C++11 a function or function object must be created

  • Defined in the global context and not necessarily where needed

int cnt; for_each(v.begin(), v.end(), [](int i) -> void { cout << i << endl; // ++cnt; });

Return type may be left out.

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

222

slide-40
SLIDE 40

Algorithms: Maximum with for_each

§ f is another unary function

int my_max_val; template <typename T> void my_max(const T &x) { if (x>my_max_val) my_max_val = x; } int main(int argc, char *argv[]) { // … my_max_val = *v1.begin(); for_each(v1.begin(), v1.end(), my_max); } template <typename In, typename Op> Op for_each(In first, In last, Op f);

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

223

slide-41
SLIDE 41

Algorithms: Maximum with for_each (cont’d)

§ f can also be an instance of a class that implements operator()/1 § A so-called function object

template <class T> struct my_max

: public unary_function<T, void> {

T max; my_max(const T& x) : max(x) {} void operator() (const T& x) { if(x>max) max=x; } } int main(int argc, char *argv[]) { // … my_max<T> my_max_obj(*v1.begin()); cout << for_each(v1.begin(), v1.end(), my_max_obj).max; } template <typename In, typename Op> Op for_each(In first, In last, Op f);

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

224

deprecated in C++11; removed from C++17

slide-42
SLIDE 42

Related: Range For (foreach) Operator (C++11)

§ Copied over from Java (which copied it over from C# (which …)) § Easier to use than C++’s standard for function § Useful in combination with initializer lists

void print(const vector<int> &v) { for(const auto &i : v) { cout << i << endl; } }

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

225

slide-43
SLIDE 43

Algorithms: transform

§ Transform input and write it to out § O(n)

template <class In, class Out, class Op> Out transform(In fst, In lst, Out res, Op o); template <class In, class In2, class Out, class BinOp> Out transform(In fst, In lst, In2 fst2, Out res, BinOp o);

algorithm

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

227

slide-44
SLIDE 44

Summary: Standard Library Algorithms

§ Discussed the Design Principles § Algorithms based on Iterators § Discussed some algorithms § Functions, function objects, and lambda functions (functors)

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

228

slide-45
SLIDE 45

BREAK?

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

229

slide-46
SLIDE 46

Exercise L3.1: Emulating pointers

§ Implement a dumb_pointer. Use the operator* and operator->

  • perators to implement a class that simulates a pointer.

§ Implement another class that works like the dumb_pointer but is called smart_pointer and uses reference counting. § No, using any of the C++ auto_ptr, unique_ptr, shared_ptr, weak_ptr classes is not an implementation option

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

230

slide-47
SLIDE 47

Exercise L3.1 (cont’d)

void print(smart_pointer<Object> p) { cout << p.counter() << ": " << *p << endl; } void foo() { Object *o1=new Object(1); // let's create our 1st object Object *o2=new Object(2); // let's create our 2nd object smart_pointer<Object> p(o1); // ref counter is 1 for 1st object cout << p.counter() << endl; // displays 1 smart_pointer<Object> q(p); // another smart pointer that points to o1 (overload copy constructor) cout << p.counter() << endl; // displays 2 (two smart pointers refer to o1) cout << q.counter() << endl; // displays 2 (two smart pointers refer to o1) smart_pointer<Object> r(o2); // ref counter is 1 for 2nd object cout << r.counter() << endl; // displays 1 q=r; // decrease counter for 1st object and // increase counter for 2nd object (overload assignment operator) cout << p.counter() << endl; // displays 1 cout << q.counter() << endl; // displays 2 cout << r.counter() << endl; // displays 2 print(p); // displays 2, and the 1st object, don't delete the object pointed to by p print(q); // displays 3, and the 2nd object, don't delete the object pointed to by q print(r); // displays 3, and the 2nd object, don't delete the object pointed to by r cout << *p << *r << endl; // display 1st and 2nd object (overload operator*) cout << p->method1() << q->method2() << r->method3() << endl; // invoke method1 on 1st object and // invoke method2 on 2nd object and // invoke method3 on 2nd object (overload operator->) } // now the destructors of p, q, and r are called, make sure that 1st // and 2nd object is each deleted once (i.e., when the counter reaches 0)

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

231

slide-48
SLIDE 48

Exercise L3.2

§ Adapt your persistent pvector to make use of the new persister trait. § Also implement a pset that implements a persistent set (based on std::set). The set shall use the same persistence_traits as the pvector.

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

232

slide-49
SLIDE 49

Exercise L3.3: RPN<complex>

§ Make sure your RPN calculator runs fine with complex numbers § Which challenge(s) did you face? How did you solve them? § Note: It is sufficient to use only concepts we had discussed so far in the lecture!

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

233

slide-50
SLIDE 50

Exercise L3.4 (this example may be extended in future exercises)

Simple Spell Checker § Implement a simple spell checker. The spell checker takes two files as command line arguments, a dictionary file containing a list of correctly spelled words and a file whose content is to be checked. Upon startup, your program reads the words contained in the dictionary file in a pset<string>. Then it reads every word in the file to spell check, checks whether each word is correctly spelled (ie contained in the dictionary file) and if not displays it on cout. § Additionally, your program should allow the user to correct them

  • r insert them into the dictionary if spelled correctly but not in

the dictionary.

  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

234

slide-51
SLIDE 51

Exercise L3.5

§ Implement a computer player for your connect4 implementation. The computer player of this version does not have to be intelligent. At a minimum, however, the computer player should be able

  • to identify whether he can win the game by placing a stone, and
  • two strategy improvements to your liking

§ Additionally, let your computer player compete against computer players from your colleagues (yes, you may share your code in the forum)

  • Report how easy it was to adapt your colleague’s computer player

implementation

  • Who did not adhere to the specification and why
  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

235

slide-52
SLIDE 52
  • Th. Gschwind. Advanced Software Engineering with C++ Templates.

240

Next Lecture

§ Templates (Yes, again!) § Classes: Fallacies & Pitfalls Have a nice weekend! Have fun solving the examples! See you in two weeks!