450 likes | 579 Views
Templates Containers Iterators (TIC++V1:C16). Yingcai Xiao 06/26/2008. Do you know?. Why Java forces all classes subclass from the Object class? How to implement an operator? How to create a generic container? Object code reuse Source code reuse Friends Enum Iterator.
E N D
TemplatesContainersIterators(TIC++V1:C16) Yingcai Xiao 06/26/2008
Do you know? Why Java forces all classes subclass from the Object class? How to implement an operator? How to create a generic container? Object code reuse Source code reuse Friends Enum Iterator
Introduction to Templates : What’s wrong? class IntStack { static const int size = 100; // size has to be known at compile time for an array int stack[size]; int top; public: IntStack() : top(0) {} void push(int i) { stack[top++] = i; } int pop() { return stack[--top]; } }; int main() { IntStack is; for(int i = 0; i < 20; i++) is.push(i); for(int k = 0; k < 20; k++) cout << is.pop() << endl; } Example: StackInt.cpp
Introduction to Templates • Problem #1: the stack could overflow. • The problem is easy to solve. • Just make the size of the stack variable. • To do that we need to use dynamic memory allocation to increase the size of the stack if “top” reaches “size”. • IntStack is a container class. • All container class should not overflow, i.e., can contain as many number of items as needed.
Introduction to Templates • Problem #2: need to repeat the same code for float, double or any other types. Code reuse? • Can we use inheritance or composition to solve the problem? • No, because they provide a way only to reuse object code. • When we say “reuse object code” we mean “the size/type of the memory can’t be changed (even though the values in the memory can be changed)” . Can we really reuse the same source code for different types? There are two solutions to the problem: Object-based hierarchy in SmallTalk and Java Template in C++ and Generic in C#
Object-based Hierarchy // In the library, to be reused code: a Stack that can store any “Object”. class Object { public: virtual void Print() {cout << "I am an object. \n"; } }; class Stack { static const int size = 100; Object* stack[size]; int top; public: Stack() : top(0) {} void push(Object* op) { stack[top++] = op; } Object* pop() { return stack[--top]; } }; Example: StackObjectBsaed.cpp
Object-based Hierarchy // In an application, using the Stack for two different types of “Object”s. class IntObject : public Object { int i; public: IntObject(int i) { this->i = i; } void Print() {cout<<"Int value = " << i << endl;} }; class FloatObject : public Object { float f; public: FloatObject(float f) { this->f = f; } void Print() {cout<<"Float value = " << f << endl;} }; Example: StackObjectBsaed.cpp
Object-based Hierarchy // In an application, using the Stack for two different types of “Object”s. int main() { Stack is; for(int i = 0; i < 8; i++)is.push(new IntObject(i)); for(int k = 0; k < 8; k++)is.pop()->Print(); Stack fs; for(int i = 0; i < 8; i++)fs.push(new FloatObject((float)(i+0.8))); for(int k = 0; k < 8; k++)fs.pop()->Print(); Stack s; for(int i = 0; i < 4; i++) { s.push(new IntObject(i));s.push(new FloatObject((float)(i+0.8))); } for(int k = 0; k < 8; k++)s.pop()->Print(); } Example: StackObjectBsaed.cpp
Object-based Hierarchy • The container can contain any types of objects as long as the types are subclassed from the Object class directly or indirectly. • The root of the hierarchy of classes is the Object class. • The container stores references to Object. • When an object is stored to the container, it is upcasted to the “Object” type. • Polymorphism will recover the real object type when the object is retrieved. • The container can store mixed types of “Object”s. • The size of a reference does not change, but the type of the objects that the references point to can vary (from one child class to another). • This is one of the reasons that Java automatically makes all classes inherits from the Object class directly or indirectly and “Class name;” declares a reference not an object.
Object-based Hierarchy • Not efficient in space usage: you have to create an IntObject object just to store an int (4 bytes). The corresponding class in Java is Integer, which inherits many methods (ToString(), GetClass(), …) from the Object class. • Not efficient in time: all objects are dynamically created as heap objects. Extra work to create and dereference at runtime. • Not efficient in coding: to use the container, programmers have to modify their existing data types to subclass from the Object class.
Templates • The template feature in C++ (TIC++V1:C16) and the generic feature in C# 2.0 (http://msdn.microsoft.com/en-us/library/512aeb7t(VS.80).aspx) provide a way to reuse source code, meaning we only have to write the same source code once for all different types. • The benefit of this type of solution compared to the object-based method are: • Efficient in space usage: you don’t have to create an IntObject object to store an int (4 bytes). • Efficient in time: all objects can be created at compile time as stack objects. No extra work to create and dereference at runtime. • Efficient in coding: to use template containers, programmers don’t need to modify their existing data types.
Template Syntax • Declaration: template<class T> • T is a substitution parameter and represents a type name. • T will be used in a class or method where you would normally see the specific type names. template<class T> class MyTemplate { T x; }; • MyTemplate<int> y; • // y is an object of MyTemplate with y.x instantiated as an int.
Templates How does it work? • The compiler makes the substitution of T to a real type name • The process is also called instantiation of the template. • It is done as part of the compilation process but before the regular compilation. This is so called 2-pass compilation: instantiationand compilation. • The instantiation is done after preprocessing and is different from preprocessing. • The instantiation of a template is the replacement of the type parameter with a real type name (no memory allocation of any kind). • The instantiation of a class allocates the memory for an object of the class.
Template Stack #ifndef STACKTEMPLATE_H #define STACKTEMPLATE_H template<class T> class StackTemplate { enum { size = 100 }; // another way of making size known at compile time for an array T stack[size]; int top; public: StackTemplate() : top(0) {} void push(const T& i) {stack[top++] = i;} T pop() {return stack[--top];} int size() { return top; } }; #endif // STACKTEMPLATE_H Example: StackTemplate.h
Template Stack #include "StackTemplate.h" #include <iostream> #include <fstream> #include <string> using namespace std; int main() { // use as an int stack StackTemplate<int> is; for(int i = 0; i < 8; i++) is.push(i); for(int k = 0; k < 8; k++) cout << is.pop() << endl; ifstream in("StackTemplate.cpp"); string line; // use as a string stack StackTemplate<string> strings; while(getline(in, line)) strings.push(line); while(strings.size() > 0) cout << strings.pop() << endl; } Example: StackTemplate.h
Thinking in Templates • A template is basically a type parameter. • The concept of type parameters makes it possible to design classes and methods that defer the specification of one or more types until the class or method is declared and instantiated by client code. • By using a type parameter T you can write a single class that other client code can use without incurring the cost or risk of runtime casts or type conversions. • The template type is substituted/instantiated at compile time, therefore no runtime overhead. • One parameter can only represent one type, therefore a template stack can only hold one type of objects for each instantiation. An instantiated stack can’t hold objects of different types. • The stack example here stores the values not the pointers of the objects. You may ended with duplicated objects if you keep copies of the objects outside the stack. • The logic can be applied to develop one that stored pointers to objects.
Constants in Templates • Template arguments are not restricted to class types • You can also use built-in types. • The values of these arguments then become compile-time constants for that particular instantiation of the template. • You can even use default values for these arguments. • template<class T, int size = 100> • class StackTemplate { • T stack[size]; • int top; • public: • StackTemplate() : top(0) {} • void push(const T& i) {stack[top++] = i;} • T pop() {return stack[--top];} • int size() { return top; } • }; Example: StackTemplate2.h
Constants in Templates #include "StackTemplate2.h" #include <iostream> #include <fstream> #include <string> using namespace std; int main() { StackTemplate<int, 8> is; for(int i = 0; i < 8; i++) is.push(i); for(int k = 0; k < 8; k++) cout << is.pop() << endl; ifstream in("StackTemplate.cpp"); string line; StackTemplate<string> strings; while(getline(in, line)) strings.push(line); while(strings.size() > 0) cout << strings.pop() << endl; } Example: StackTemplate2.cpp
Template Function Defining a template function: template<class T> T max (T a, T b) { return (a > b ? a : b) ; } Use of a template function: cout << max(2,3); cout << max(2.3,3.4);
Templates of more than one type parameters Defining a template of more than one type parameters: template<class T, class U> T max (T a, U b) { return (a > b ? a : b) ; } Use of a template of more than one type parameters: cout << max(8.8, 2); cout << max(2, 8.8); // anything wrong here?
Enum • User-defined type consisting of a set of named integer constant (enumerators) • Declararion: • enum enum-type-name { enum-list } enum-variable; • enum enum-type-name { enum-list }; • // enum-type-name is optional. • Usage: • // declaration of a variable of the type enum-type-name • enum-type-name x; • // assigning a value to the variable • x = a member of the enum-list;
Enum #include <iostream> using namespace std; int main() { enum Days {Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday}; Days d; d = Sunday; cout << "Sunday: " << d << endl; d = Monday; cout << "Monday: " << d << endl; enum Days1 {Sunday1 = 1, Monday1, Tuesday1, Wednesday1, Thursday1, Friday1, Saturday1}; Days1 d1; d1 = Sunday1; cout << "Sunday1: " << d1 << endl; d1 = Monday1; cout << "Monday1: " << d1 << endl; enum Days2 {Sunday2 = 10, Monday2 = 20, Tuesday2, Wednesday2, Thursday2, Friday2, Saturday2}; Days2 d2; d2= Sunday2; cout << "Sunday2: " << d2<< endl; d2 = Tuesday2; cout << "Tuesday2: " << d2 << endl; Example: enum.cpp
Enum // d2++; // will not compile, operator not defined // d2 = Thursday1; // will not compile, can't convert Days1 to Days2 // d2 = 10; // will not compile, can't convert int to Days2 int i = Monday2; cout << "i = " << i << endl; i = Monday; cout << "i = " << i << endl; i ++; cout << "i = " << i << endl; }
Enum • Compiler make sure only a value from the list is assigned to a enum variable. The variable is always in “range”. • The constants can be assigned to any int variables. But the compiler will not be able to make the check and the int variable may be manipulated to have an value out of the “range” of the list. • enum takes no space and is resoved at compile time. More efficient than static int. • class IntStack { • enum { size = 100 }; • int stack[size]; • // … • } • class IntStack { • static const int size = 100; • int stack[size]; • //… • }
Operators: • An operator is a function. • operatorOperatorName is the name of the function internally. • e.g. operator++ is the name of the function internally represents ++. • The operator (e.g. ++) can only be operated on objects of the class where the operator is defined. “this” pointer: • “this” pointer is defined internally. • Passed as the first argument (hidden) of all functions of a class. • “this” pointer points to the address of the current object.
Friend • “friend” is a reserved key word in C++ to give full access of any member of a class (private or not) to someone else (a class or a function). • class A { • private: static string Hello () { return "Hi, there!"; } • friend class B; // or friend B • }; • class B { • public: string SayHi () { return A::Hello(); } • }; • int main() { • B b; • cout << b.SayHi()<< endl; • }
Forward Declaration • Any type you are using should be declared before being used (for the compiler to know what it is.) • Class A refers to B and Class B refers to A. Which one should be declared first? • Use forward declaration or class name definition (not class definition). • class B; • class A { • private: static string Hello () { return "Hi, there!"; } • friend B; // or friend class B • }; • class B { • public: string SayHi () { return A::Hello(); } • }; • int main() { • B b; • cout << b.SayHi()<< endl; • }
Nested Class • Nested classes are classes inside a class. • class A { • int x; • public: A () { x = 7; cout << " x initialized.\n"; } • int getX() { return x; } • class B { • int y; • public: B() { y=8; cout << " y initialized.\n"; } • int getY() {return y;} • B* getb() {return this;} • }b; // end B • }; // end A • int main() { A o; • A::B *bp = o.b.getb(); • cout << "o.b.getY() = " << o.b.getY() << endl; • cout << "bp->getY() = " << bp->getY() << endl; • }
Nested Class • The memory layout of o: • b is part of o. • bp stores the address of b and hence points to b. • No special access privileges to member functions of the nested class or the enclosing class. • But some compilers (g++ and VC++) treat the nested class as a full member of the enclosing class, therefore give the member functions in the nested class full access to the members of the enclosing class (including the private ones). o b bp
Constructor Initializer List • The list is initialized before any statements of the constructor is processed. • A primitive/built-in type can be initialized as if it has an constructor. • class IntStackIter { • IntStack& s; // private • int index; // private • public: • IntStackIter(IntStack& is) : s(is), index(0) { /* other statements */ } • // s = is, index = 0; • // … • };
Iterators • An iterator is an object that moves through a container of other objects and selects them one at a time, without providing direct access to the implementation of that container. • Iterators provide a standard way to access elements, whether or not a container provides a way to access the elements directly. • Iterators are used most often in association with container classes, and iterators are a fundamental concept in the design and use of the Standard C++ containers. • Iterator is a design pattern that promotes code reuse by accessing containers in a unified manner.
Iterators #include <iostream> using namespace std; class IntStack { enum { ssize = 100 }; int stack[ssize]; int top; public: IntStack() : top(0) {} void push(int i) { stack[top++] = i; } int pop() { return stack[--top]; } friend class IntStackIter; }; Example: iterator.cpp
Iterators // An iterator is like a "smart" pointer: class IntStackIter { IntStack& s; int index; public: IntStackIter(IntStack& is) : s(is), index(0) {} int operator++() { // Prefix return s.stack[++index]; } int operator++(int) { // Postfix return s.stack[index++]; } }; Example: iterator.cpp
Iterators int main() { IntStack is; for(int i = 0; i < 8; i++) is.push(i); // Traverse with an iterator so that the code is reusable. IntStackIter it(is); for(int j = 0; j < 8; j++) cout << it++ << endl; } Example: iterator.cpp
Iterator Improvements • To aid in making things more generic, it would be nice to be able to say “every container has an associated class called iterator,” but this will typically cause naming problems. • The solution is to add a nested iterator class to each container (notice that in this case, “iterator” begins with a lowercase letter so that it conforms to the style of the Standard C++ Library). • We also need to make the stack generic instead of just for int.
Iterator Improvements // Simple stack template with nested iterator #ifndef ITERSTACKTEMPLATE_H #define ITERSTACKTEMPLATE_H #include <iostream> template<class T, int ssize = 100> class StackTemplate { T stack[ssize]; int top; public: StackTemplate() : top(0) {} void push(const T& i) { stack[top++] = i; } T pop() { return stack[--top]; } IterStackTemplate.h
Iterator Improvements class iterator; // Declaration required friend class iterator; // Make it a friend class iterator { // Now define it StackTemplate& s; int index; public: iterator(StackTemplate& st): s(st),index(0){} // To create the "end sentinel" iterator: iterator(StackTemplate& st, bool) : s(st), index(s.top) {} T operator*() const { return s.stack[index];} T operator++() { // Prefix form return s.stack[++index]; } T operator++(int) { // Postfix form return s.stack[index++]; } IterStackTemplate.h
Iterator Improvements // Jump an iterator forward iterator& operator+=(int amount) { index += amount; return *this; } // To see if you're at the end: bool operator==(const iterator& rv) const { return index == rv.index; } bool operator!=(const iterator& rv) const { return index != rv.index; } friend std::ostream& operator<<( std::ostream& os, const iterator& it) { return os << *it; } }; iterator begin() { return iterator(*this); } // Create the "end sentinel": iterator end() { return iterator(*this, true);} }; #endif // ITERSTACKTEMPLATE_H ///:~ IterStackTemplate.h
Iterator Improvements //: C16:IterStackTemplateTest.cpp #include "IterStackTemplate.h" #include <iostream> #include <fstream> #include <string> using namespace std; int main() { StackTemplate<int> is; for(int i = 0; i < 20; i++) is.push(i); // Traverse with an iterator: cout << "Traverse the whole StackTemplate\n"; StackTemplate<int>::iterator it = is.begin(); while(it != is.end()) cout << it++ << endl; IterStackTemplateTest.cpp
Iterator Improvements cout << "Traverse a portion\n"; StackTemplate<int>::iterator start = is.begin(), end = is.begin(); start += 5, end += 15; cout << "start = " << start << endl; cout << "end = " << end << endl; while(start != end) cout << start++ << endl; ifstream in("IterStackTemplateTest.cpp"); string line; StackTemplate<string> strings; while(getline(in, line)) strings.push(line); StackTemplate<string>::iterator sb = strings.begin(), se = strings.end(); while(sb != se) cout << sb++ << endl; } ///:~ IterStackTemplateTest.cpp
Iterator Improvements • The * operator selects the current element, which makes the iterator look more like a pointer and is a common practice. • You can “jump” an iterator forward by an arbitrary number of elements using operator+=. • Two overloaded operators: == and != compare one iterator with another. They are primarily intended as a test to see if the iterator is at the end of a sequence in the same way that the “real” Standard C++ Library iterators do. The idea is that two iterators define a range, including the first element pointed to by the first iterator and up to but not including the last element pointed to by the second iterator. • Note that the end iterator, which we often refer to as the end sentinel, is not dereferenced and is there only to tell you that you’re at the end of the sequence. Thus it represents “one past the end.”
Iterator Improvements • The container member function begin( ) uses the first iterator constructor that defaults to pointing at the beginning of the container (this is the first element pushed on the stack). • However, a second constructor, used by end( ), is necessary to create the end sentinel iterator. Being “at the end” means pointing to the top of the stack, because top always indicates the next available – but unused – space on the stack. This iterator constructor takes a second argument of type bool, which is a dummy to distinguish the two constructors.
Summary • What? How? Why? Inside? • Template • Container • Iterator • object code reuse • source code reuse • object-based hierarchy • enum • operator • friend • nested class