Advanced Software Engineering with C++ Templates
Templates II: Traits Thomas Gschwind <thgatzurichdotibmdotcom>
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++
Templates II: Traits Thomas Gschwind <thgatzurichdotibmdotcom>
182
§ Specialization § Traits § Default Values
§ 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
§ What is an interesting data type for T? § Why is it interesting?
183
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() {
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
184
§ 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 }
185
§ The following shows a sample child class inheriting from a sample base class
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.
§ Use inheritance
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() { … } };
187
Not bad, but we can do better than that
§ Factor out the persistence logic into a separate interface
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; };
188
§ 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); } … };
189
§ 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
template<typename T> struct element_serializer { virtual void read(ifstream &i, T &elem) { … }; virtual void write(ofstream &o, const T &elem) { … }; };
190
§ Templates can also be used to provide polymorphism
§ 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)
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
§ First, let’s refactor the pvector class to move the read and write methods into external classes…
192
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() {
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
193
§ Factor out the persistence logic into a separate class
§ But what if we want to use different persisters?
194
§ 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() {
vector<T>::iterator fst=v.begin(), lst=v.end(); while(fst!=lst) P::write(ofs, *fst++); } } pvector<string, persistence_traits<string> > avector;
195
§ Make the persistence class generic
196
§ 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() {
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 =============================
197
§ 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() {
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
198
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
§ 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
199
200
Standard Library: Algorithms, Functors, Design Thomas Gschwind <thgatzurichdotibmdotcom>
202
§ Design Principles § Iterators § Algorithms § Function Operators
§ Containers alone are useful but not so useful
§ Examples
§ How shall we solve it?
203
§ 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; }
204
§ 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; }
205
§ Work like a pointer § Implement the following operations (at least)
§ Note: std::iterator is a convenience class that provides a set
difference_type, pointer, and reference
207
§ 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
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
auto val=*begin; // C++11, or typename Iter::value_type val=*begin;
§ 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”
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; }
§ 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; }
211
§ 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; }
212
begin==beyond at this point. We can use this to shorten the algorithm further.
§ 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; }
213
§ Find an element that meets a given condition § Create an interface and pass object implementing that interface
214
§ Allow user to pass a function (pred) that evaluates the condition
use a pointer of the type of the function
can be declared as bool (*pred)(int)
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; }
§ Need to always be a function, looks complicated § Function always called through a function pointer § Template parameters are not limited to just classes
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; }
§ Now, pred may be a
template<typename Iter, typename Pred> Iter find_if(Iter begin, Iter beyond, Pred pred) { while (begin!=beyond) { if (pred(*begin)) break; ++begin; } return begin; }
218
§ Non mutating
§ Mutating
§ Sorting
§ Sets
Decide which procedures you want; use the best algorithms you can find. Bjarne Stroustrup
219
§ Calls f for each element in [first, last) § O(n) § Attention!
template <typename In, typename Op> Op for_each(In first, In last, Op f);
220
§ 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);
221
§ [] captures the environment that visible within the lambda function
§ Compiler generates a function object for each lambda function § Prior to C++11 a function or function object must be created
int cnt; for_each(v.begin(), v.end(), [](int i) -> void { cout << i << endl; // ++cnt; });
Return type may be left out.
222
§ 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);
223
§ 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);
224
deprecated in C++11; removed from C++17
§ 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; } }
225
§ 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
227
§ Discussed the Design Principles § Algorithms based on Iterators § Discussed some algorithms § Functions, function objects, and lambda functions (functors)
228
229
§ Implement a dumb_pointer. Use the operator* and operator->
§ 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
230
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)
231
§ 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.
232
§ 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!
233
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
the dictionary.
234
§ 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
§ Additionally, let your computer player compete against computer players from your colleagues (yes, you may share your code in the forum)
implementation
235
240
§ Templates (Yes, again!) § Classes: Fallacies & Pitfalls Have a nice weekend! Have fun solving the examples! See you in two weeks!