Download presentation
Presentation is loading. Please wait.
Published byKyle Strickland Modified over 10 years ago
1
Introduction to C++ Templates Speaker: Bill Chapman, C++ developer, financial company in Mid-Manhattan. Can be reached via: cplusplus@ccjj.info ' This talk will be based entirely on C++ as of 2003 – no C++11 will be used. This will be an introduction to templates. It is assumed the audience Knows C (including pointer arithmetic). Understands C++ classes, methods, constructors, destructors, references, and 'const'. Is familiar with C++ I/O & the C++ 'std::string' class.
2
About Templates Hard to program. Horrible, very long error messages on mistakes. Most C++ programmers rarely write template code, mostly it's pre-done for them in libraries, which are pretty easy to use. The STL is the most famous C++ template library, and is pretty much always included nowadays if you install C++ on your machine.
3
Outline 1)'max' function template. 2)'mySort' function template. 3) Container class template 'MyArray'. 4) Specialized templates to identify categories of types. 5) Using type category identification to enhance 'MyArray'. 6) Introduction to the STL. 7) Generating primes at compile-time via templates. 8) Generating primes at compile-time without even running the executable (making the primes show up in compiler error messages).
4
Approaches to 'max' function We want to have a function to take the maximum of two numbers. One way to do it is with a macro #define MAX(LHS, RHS) ((LHS) > (RHS) ? (LHS) : (RHS)) This has some disadvantages Not scoped. If anyone, anywhere, declares an identifier named MAX after that, global or local, they'll get really confusing compile errors. 'Winner' evaluated twice: 'MAX(++i, ++j)', or 'MAX(slowFunction(x), slowFunction(y))'
5
'double' function Another approach is to declare an inline typed function: inline double max(double x, double y) { return x > y ? x : y; } Inefficient if you were just dealing with 'int's. What if you want to take the 'max' of a type that stored unlimited precision ints – can't reliably be converted to and from a double.
6
Solution: Function Template template inline T max(const T& lhs, const T& rhs) { return lhs > rhs ? lhs : rhs; } The compiler will match 'T' to any type that is passed to the args of this routine, and create a routine for that type. Drawback – what if T is a huge class – say, big tables for which 'operator>' is defined? We're copying the return value, which is expensive. So we do: template inline const T& max(const T& lhs, const T& rhs) { return lhs > rhs ? lhs : rhs; }
7
Problems With 'max' Template ' max(4, 7.3)' won't work – passing an int and a float, different types. Template doesn't know which to use, refuses to compile. Templates won't convert types AT ALL when computing types. So suppose we template inline const A& max(const A& a, const B& b) { return a > b ? a : b; } NOT a good idea – arbitrarily chose 'A', not 'B' as the return type, so 'max(4, 7.3)' will return 7! So we only use the previous implementation.
8
Making one-type 'max' work So we go back to our previous implementation, which is basically what's available in the STL in '#include '. template inline const T& max(const T& lhs, const T& rhs) { return lhs > rhs ? lhs : rhs; } To make 'max(4, 7.3)' work, you have to be explicit and explain to the compiler exactly what return type you want: max((double) 4, 7.3) // '4' cast to double, 7.3 max (4, 7.3) // '4' cast to double, 7.3
9
What If Function Template Not Inline? template const T& max(const T& lhs, const T& rhs) { return lhs > rhs ? lhs : rhs; } When the function is called, the compiler will generate code for a version of the function with the appropriate 'T'. If two different.cpp files call max, there will be multiple copies of the code for the function generated, all but one of which will be discarded by the linker. There are ways to tell the compiler not to generate code for a function in the current module, basically promising that another module will do it. We won't get into that. The way people usually do function templates, inline or not, is with the code implementing it visible in the.h file, and let the linker sort out redundancies.
10
Unix 'qsort' C function void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); Can't handle types with complex assignment, copy c'tor, or d'tor -- must be bitwise copyable. No 'std::string's, for example (std::string MIGHT work, sort of by accident, depending on the details of the implementation of 'qsort' and 'std::string'). Have to cast pointers to 'void *'s, throwing away type checking Have to provide a special 'compar' function, which must take two 'void *' ptrs, and it cannot be inline.
11
Bubble Sort Function Template This function will sort an array, given ptrs to beginning and end. template void mySort(TYPE *begin, TYPE * const end) { for (; begin != end; ++begin) { for (TYPE *rover = begin + 1; rover != end; ++rover) { if (*rover < *begin) { // 'operator<' TYPE tmp(*rover); // copy c'tor *rover = *begin; // assignment *begin = tmp; // assignment } // destructor called on 'tmp' here } } } Note uses 'operator<', copy c'tor, assignment, and d'tor. All can be inline!!! This could be trivially upgraded to a better sorting algorithm. Type checking on args, good chance you wanted to implement 'operator<' for TYPE anyway. STL has 'std::sort' in '#include ', along these lines, but much better.
12
Example: Container Class Arrays of complex types are hard to do by hand in C++. Want to be able to efficiently grow & shrink array. 'new[]' must call default c'tor when you allocate array – might prefer to defer c'tor calling until you call copy c'tor when adding elements. If forget to destroy something, could leak memory. Hard to grow & shrink an array – lots of copying, have to do just right, call appropriate c'tors & d'tors. Well create a 'container class' template that will do most of this work for you, for any well-behaved type, including complex types. We'll call it 'MyArray'. Showing the code to implement 'MyArray' will take several slides.
13
Properties of 'MyArray' Has two properties: 'capacity' and 'size'. 'size' is how many elements there are in the container. 'capacity' is how many elements there is room for. Never calls default c'tor. Only copies copy c'tor when it actually has an element to store into the container. When container is destroyed, destroys all elements contained. 'capacity' can only grow, never shrinks.
14
MyArray: Class Declaration template class MyArray { // DATA unsigned d_capacity; unsigned d_size; TYPE *d_array; // PRIVATE MANIPULATORS void grow(); public: // CREATORS MyArray(); ~MyArray(); // MANIPULATORS TYPE& operator[](unsigned i); void push_back(const TYPE& t); void pop_back(); // ACCESSORS const TYPE& operator[](unsigned i) const; unsigned size() const; };
15
MyArray Imp: C'tor, D'tor // CREATORS template MyArray ::MyArray() : d_capacity(4), d_size(0) { // use malloc, not 'new' -- don't want c'tors run on // individual elements d_array = (TYPE *) malloc(d_capacity * sizeof(TYPE)); } template MyArray ::~MyArray() { TYPE * const end = d_array + d_size; for (TYPE *pt = d_array; pt ~TYPE(); } free(d_array); }
16
MyArray Imp: Manipulators // MANIPULATORS template TYPE& MyArray ::operator[](unsigned i) { return d_array[i]; } template void MyArray ::push_back(const TYPE& val) { if (d_size == d_capacity) { grow(); } new (d_array + d_size) TYPE(val); // placement new – calls copy // c'tor, doesnt allocate ++d_size; } template void MyArray ::pop_back() { --d_size; (d_array + d_size)->~TYPE(); // explicit d'tor call }
17
MyArray Imp: Accessors // ACCESSORS template const TYPE& MyArray ::operator[](unsigned i) const { return d_array[i]; } template unsigned MyArray ::size() const { return d_size; }
18
MyArray Imp: 'grow' // PRIVATE MANIPULATORS template void MyArray ::grow() { unsigned newCapacity = d_capacity * 2; TYPE *newArray = (TYPE *) malloc(sizeof(TYPE) * newCapacity); TYPE * const end = d_array + d_size; for (TYPE *pTo = newArray, *pFrom = d_array; pFrom ~TYPE(); // explicit call to d'tor } free(d_array); d_array = newArray; d_capacity = newCapacity; }
19
Using MyArray: myArray.cpp #include "mySort.h" #include "myArray.h" int main() { MyArray mv; const char *data[] = { "woof", "arf", "meow", "grrrr", "chomp", "bite", "maul", "paw", "tear", "mangle", run, sniff }; enum { NUM_DATA = sizeof(data) / sizeof(data[0]) }; for (int i = 0; i < NUM_DATA; ++i) { mv.push_back(data[i]); // const char * gets converted to std::string } assert(mv.size() == NUM_DATA); mySort(&mv[0], &mv[0] + mv.size()); // verify mv is sorted for (int i = 0; i < mv.size() - 1; ++i) { assert(mv[i] <= mv[i + 1]); } // print it out std::cout << // Produces Output:\n; for (unsigned i = 0; i < mv.size(); ++i) { std::cout << "// " << mv[i] << std::endl; } // d'tor called on all the strings in 'mv', and the array in 'mv' is // freed when 'mv' goes out of scope. }
20
MyArray.cpp Ouput // Produces Output: // arf // bite // chomp // grrrr // mangle // maul // meow // paw // run // sniff // tear // woof
21
Type Templates vs Function Templates When a function template is called, the compiler can often infer the 'TYPE' from the types of the arguments passed. int x = std::max(3, 7); When declaring a variable of a templatized type, you must explitly specify the type(s) of the template arg(s). MyArray mv;
22
Template Specialization: IsPointer You can define the same template multiple times, with different types as args. The compiler will choose the most specialized one to call. template // this template will match any 'TYPE' struct IsPointer { enum { VAL = 0 }; }; template // more specialized: only matches pointers struct IsPointer { enum { VAL = 1 }; };... assert(0 == IsPointer ::VAL); assert(1 == IsPointer ::VAL);
23
Template Specialization: IsFundamental template struct IsFundamental { enum { VAL = 0 }; }; template <> struct IsFundamental enum { VAL = 1 }; }; template <> struct IsFundamental enum { VAL = 1 }; }; template <> struct IsFundamental enum { VAL = 1 }; }; // Repeat for long, long long, unsigned char, signed char, unsigned short, unsigned, unsigned long, // unsigned long long, float. template <> struct IsFundamental enum { VAL = 1 }; };... assert(1 == IsFundamental ::VAL); assert(1 == IsFundamental ::VAL); assert(0 == IsFundamental ::VAL); assert(0 == IsFundamental ::VAL); assert(0 == IsFundamental ::VAL);
24
What About 'const'? #include "isPointer.h" #include "isFundamental.h" struct S { int d_i; }; int main() { assert(0 == IsPointer ::VAL); assert(1 == IsPointer ::VAL); assert(1 == IsPointer ::VAL); // const OK assert(0 == IsPointer ::VAL); assert(1 == IsPointer ::VAL); assert(1 == IsPointer ::VAL); // const OK assert(0 == IsFundamental ::VAL); assert(0 == IsFundamental ::VAL); assert(0 == IsFundamental ::VAL); assert(1 == IsFundamental ::VAL); assert(1 == IsFundamental ::VAL); // FAILS!!! assert(1 == IsFundamental ::VAL); // FAILS!!! }
25
Fix 'IsFundamental' for 'const' template struct IsFundamentalImp { enum { VAL = 0 }; }; template <> struct IsFundamentalImp enum { VAL = 1 }; }; template <> struct IsFundamentalImp enum { VAL = 1 }; }; // Repeat for int, long, long long, unsigned char, signed char, unsigned short, unsigned, // unsigned long, unsigned long long, float. template <> struct IsFundamentalImp enum { VAL = 1 }; }; template struct IsFundamental { enum { VAL = IsFundamentalImp ::VAL }; };
26
'const' Now Works #include "isPointer.h" #include "isFundamental.h" struct S { int d_i; }; int main() { assert(0 == IsPointer ::VAL); assert(1 == IsPointer ::VAL); assert(1 == IsPointer ::VAL); // const OK assert(0 == IsPointer ::VAL); assert(1 == IsPointer ::VAL); assert(1 == IsPointer ::VAL); // const OK assert(0 == IsFundamental ::VAL); assert(0 == IsFundamental ::VAL); assert(0 == IsFundamental ::VAL); assert(1 == IsFundamental ::VAL); assert(1 == IsFundamental ::VAL); // const OK!!! assert(1 == IsFundamental ::VAL); // const OK!!! }
27
Optimizing 'MyArray' With Specializations In the implementation of 'MyArray', we can check if the parametrized type is a pointer or fundamental type. If it is, we know We never have to call destructors on elements. A bitwise copy will work just as well as the copy constructor. Using this information, we can produce a more optimized implementation for containers of simple types.
28
Optimizing 'MyArray' With Specializations The methods where we stand to gain from this optimization are 'grow' and the destructor. First, grow. New code in red: template void MyArray ::grow() { unsigned newCapacity = d_capacity * 2; TYPE *newArray = (TYPE *) malloc(sizeof(TYPE) * newCapacity); if (0 == IsPointer ::VAL && 0 == IsFundamental ::VAL) { TYPE * const end = d_array + d_size; for (TYPE *pTo = newArray, *pFrom = d_array; pFrom ~TYPE(); } } else { memcpy(newArray, d_array, d_size * sizeof(TYPE)); } free(d_array); d_array = newArray; d_capacity = newCapacity; }
29
Optimizing 'MyArray' With Specializations Now, the destructor. New code in red: template MyArray ::~MyArray() { if (0 == IsPointer ::VAL && 0 == IsFundamental ::VAL) { TYPE * const end = d_array + d_size; for (TYPE *pt = d_array; pt ~TYPE(); } } free(d_array); }
30
The STL The C++ Standard Template Library Has functions in '#include - std::max - std::min - std::sort // and many, many others Has many containers - std::vector // like 'MyArray', but much more powerful - std::set // binary tree, keys only - std::map // binary tree, key + data - std::list // doubly-linked list - std::deque // double-ended queue (pronounced 'deck') - std::hash_set // hash table, keys only, to be replaced by std::unordered_set - std::hash_map // hash table key + data, to be replaced by std::unordered_map // and more Doc: http://www.sgi.com/tech/stl/ http://www.cplusplus.com/reference/
31
Doing Our Sort With the STL #include int main() { const char *data[] = { "woof", "arf", "meow", "grrrr", "chomp", "bite", "maul", "paw", "tear", "mangle", "sniff", "run" }; enum { NUM_DATA = sizeof data / sizeof *data }; std::vector mv(data + 0, data + NUM_DATA); assert(mv.size() == NUM_DATA); std::sort(&mv[0], &mv[0] + mv.size()); // verify mv is sorted for (unsigned u = 0; u < mv.size() - 1; ++u) { assert(mv[u] <= mv[u + 1]); } std::cout << "// Produces output:\n"; for (unsigned u = 0; u < mv.size(); ++u) { std::cout << "// " << mv[u] << std::endl; } } Note the constructor for 'mv.
32
Programming With STL Containers Programmers learn to use the STL Containers as building blocks. All containers are well-behaved, general-purpose types, with =, ==,, =, != all defined. Easy to iterate through the elements in any STL container with a loop. Generally much simpler to build a datastructure out of STL Containers than to build it with naked pointers. And you rarely have to worry about destructing anything or leaking memory. The containers do it all for you. It is quite common to have a set of vectors, or a map of maps – all combinations are possible.
33
Calculating Primes At Compile Time Everything we've covered so far is potentially extremely useful. Calculating primes at compile time is not – it's MUCH easier to calculate them at run-time. Someone figured out, and mathematically proved, that you can simulate a turning machine with templates at compile time. So in theory you can do anything. In practice, it's an incredibly inefficient (but quite fun) way to do things. Just don't quit your day job.
34
simplePrime.h Note that templates can take 'int' and 'bool' for template parameters as well as types. They can't take floats or doubles. template struct IsPrime_Aux { enum { VAL = (NUM % DIV) && IsPrime_Aux ::VAL }; }; template struct IsPrime_Aux { // terminates recursion enum { VAL = 1 }; }; template struct IsPrime { enum { BOTTOM = NUM = 2 && IsPrime_Aux ::VAL }; }; The lowest prime number is 2. 0 and 1 are not prime. ' IsPrime ::VAL ' will be 1 if 'NUM' is prime and 0 otherwise. This is the simplest way I could think of to do it.
35
Calling 'IsPrime' How do we loop to call 'IsPrime' for a range of numbers? How about: for (int i = 0; i ::VAL) std::cout << i << std::endl; } Anybody see why there's a problem with this? 'i' is a variable – only types or compile-time constants can be passed in the '<>'s to a template. We could just line up 100 calls by hand, but we're programmers – such an approach is beneath us. So how do we iterate from 0 to 99 at compile- time?
36
simplePrimes.cpp #include "simplePrime.h" template struct ShowPrimes { static void run() { ShowPrimes ::run(); if (IsPrime ::VAL) std::cout struct ShowPrimes { static void run() { if (IsPrime ::VAL) std::cout ::run(); } $./a.out 2 3 5 7 11 13 17 19,,,
37
betterPrime.h template struct IsPrime { template NUM)> struct Aux; // unspecialized declaration, no definition template struct Aux { enum { VAL = (NUM % DIV) && Aux ::VAL }; }; template struct Aux { enum { VAL = 1 }; }; enum { VAL = 2 == NUM || (NUM > 1 && NUM % 2 && Aux ::VAL) }; }; This implementation of 'IsPrime ' is much more efficient than the last one. It special cases the check for even numbers, and other than that only tries to divide odd numbers, up past the square root of 'NUM', at which point it stops, where 'simplePrime.h' tried dividing all numbers, odd and even, from 2 up to 'NUM – 1'. The means of stopping recursion is interesting. We use a specialized template with the 2nd boolean argument 'DONE' specialized to detect when the divisor has risen past the square root of 'NUM', then recursion terminates.
38
betterPrime.cpp #include "betterPrime.h template struct ShowPrimes { static void run() { ShowPrimes ::run(); if (IsPrime ::VAL) std::cout struct ShowPrimes { static void run() { if (IsPrime ::VAL) std::cout ::run(); } $./a.out 2 3 5 7 11 13... Just like 'simplePrimes.cpp', except includes 'betterPrime.h' instead of 'simplePrime.h' and produces the same output 0 - 100
39
Generating Primes in Error Messages It would be fun to generate primes without creating an executable – that is, nothing gets to be done at run time. In struct 'AssertPrime' we force a compiler error by dividing by 0 if 'NUM' is prime. #include "betterPrime.h" template struct AssertPrime { enum { VAL = 1 / (0 == IsPrime ::VAL) }; }; template struct ShowPrimes { enum { A = ShowPrimes ::VAL, VAL = AssertPrime ::VAL + A }; }; template struct ShowPrimes { enum { VAL = AssertPrime ::VAL }; }; enum { VAL = ShowPrimes ::VAL }; Notice that is never included and there is no 'main'.
40
Error Messages from assertPrimes.cpp Gives rise to a horrid torrent of error messages. But notice 'AssertPrime ': assertPrimes.cpp:13:1: error: expected unqualified-id before } token assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp:10:10: recursively instantiated from ShowPrimes assertPrimes.cpp:10:10: instantiated from ShowPrimes assertPrimes.cpp:19:32: instantiated from here assertPrimes.cpp:5:10: warning: division by zero [-Wdiv-by-zero] assertPrimes.cpp:5:10: error: (1 / 0) is not a constant expression assertPrimes.cpp:5:10: error: enumerator value for VAL is not an integer constant assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp:10:10: recursively instantiated from ShowPrimes assertPrimes.cpp:10:10: instantiated from ShowPrimes assertPrimes.cpp:19:32: instantiated from here assertPrimes.cpp:5:10: warning: division by zero [-Wdiv-by-zero] assertPrimes.cpp:5:10: error: (1 / 0) is not a constant expression assertPrimes.cpp:5:10: error: enumerator value for VAL is not an integer constant assertPrimes.cpp: In instantiation of AssertPrime :...
41
Grepping Error Output for 'AssertPrime' $ g++ assertPrimes.cpp 2>&1 | grep AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime : assertPrimes.cpp: In instantiation of AssertPrime :...
42
Copyright © 2012 – Bill Chapman No rights reserved – public domain. May be modified, duplicated, sold and / or used for any purpose by anyone without permission from the author.
Similar presentations
© 2024 SlidePlayer.com. Inc.
All rights reserved.