Generic programming & library development Today: Generic programming techniques • power of templates • design patterns Lecturer: Jyrki Katajainen Some of these slides are from Kenny Erleben Course home page: http://www.diku.dk/forskning/performance-engineering/ Generic-programming/ c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (1) Polymorphism The word polymorphism means “the ability to have many forms”. Parametric polymorphism: C++ templates Inclusion polymorphism: C++ virtual functions Overloading: C++ function overloading including partial specialization Coercion: C++ built-in or user defined conversion operators or constructors to coercion c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (2) Dynamic polymorphism: base class A traditional approach where common behaviour is defined in an abstract base class class Shape { public: virtual int id() const = 0; virtual std::string type() const = 0; // ... }; c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (3) Dynamic polymorphism: derived classes class Sphere : public Shape { public: virtual int id() const { return 1; } virtual std::string type() const { return "sphere"; } }; class Box : public Shape { public: virtual int id() const { return 2; } virtual std::string type() const { return "box"; } }; and so on... (Question: Why are all member functions virtual?) c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (4) Dynamic polymorphism: test functions Let us define some functions that operate on different shapes void pair_test(Shape const* A, Shape const* B) { std::cout << "collision detection:" << (*A).type() << " and " << (*B).type() << std::endl; } Or a little more exotic void collision(std::vector<Shape*> const& shapes) { for(unsigned i = 0; i < shapes.size(); ++i) { for(unsigned j = i + 1; j < shapes.size(); ++j) { pair_test(shapes[i], shapes[j]); } } } c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (5) Dynamic polymorphism: usage Let us try our example functions int main() { Sphere s0; Sphere s1; Box b0; Box b1; Box b2; pair_test(&b2, &s1); std::vector<Shape*> shapes; shapes.push_back(&s0); shapes.push_back(&s1); shapes.push_back(&b0); shapes.push_back(&b1); shapes.push_back(&b2); collision(shapes); } c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (6) Dynamic polymorphism: summary • The interface is bounded, • the binding of interfaces is done at run time (dynamically), and • it is easy to create heterogeneous containers. • What if we want to extend with a new shape? class Prism : public Shape... • What if we want to extend with a new function? virtual point centre_of_gravity() const = 0; c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (7) Static polymorphism Let us try to use templates instead of inheritance class Sphere { public: int id() const { return 1; } std::string type() const { return "sphere"; } }; class Box { public: int id() const { return 2; } std::string type() const { return "box"; } }; c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (8) Static polymorphism: testing We also need to rewrite our test functions template <typename Shape1, typename Shape2> void pair_test(Shape1 const& A, Shape2 const& B) { std::cout << "collision detection:" << A.type() << " and " << B.type() << std::endl; } and we can now use it int main() { ... pair_test(b0, s1); ... pair_test(b2, s2); } c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (9) Static polymorphism: falling short What about? void collision(std::vector<Shape*> const& shapes) { for(unsigned i = 0; i < shapes.size(); ++i) { for(unsigned j = i + 1; j < shapes.size(); ++j) { pair_test(shapes[i], shapes[j]); } } } • Sorry, this is impossible; we cannot handle this transparently! std::vector<Shape*> const& shapes c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (10) Static polymorphism: summary • The interface is unbounded, • the binding of interfaces is done at compile time (statically), and • one cannot create heterogeneous containers. • What if we want to extend with a new shape? class Prism • What if we want to extend with a new function? bool centre_of_gravity() const { ... }; c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (11) Design pattern: bridge Decouple an abstraction from its implementation so that the two can vary independently. • Possible to provide several implementations with the same interface. • Clients can select the best implementations for their purposes. • Implementations can be smaller than the bridge (that is, pieces identical to all implementations are implemented at the bridge). c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (12) Bridge pattern implemented using inheritance B R R* realization; virtual operationA() = 0; virtual operationB() = 0; operationA(); operationB(); operationC(); implemention 1 implemention 2 operationA(); operationB(); operationA(); operationB(); Source: [Vandevoorde and Josuttis 2003, §14.4] c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (13) Bridge pattern implemented using templates R B R realization; operationA(); operationB(); operationC(); implemention 1 implemention 2 operationA(); operationB(); operationA(); operationB(); Source: [Vandevoorde and Josuttis 2003, §14.4] c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (14) Stack bridge versus stack kernel template < template < typename V, typename V, typename A = std::allocator<V>, typename A = std::allocator<V>, typename R = cphstl::list_stack<V, A> typename R = std::list<V, A> > > class stack { class list_stack { public: public: ... ... size_type size() const; typedef std::size_t size_type; bool empty() const; ... protected: size_type size() const; R kernel; ... }; }; template typename stack<V, return } <typename V, typename A, typename R> stack<V, A, R>::size_type A, R>::size() const { kernel.size(); c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (15) Design pattern: iterator Provide a way to access the elements of a container sequentially without exposing its underlying representation. • In the C++ standard library, iterators come in several different flavours: locators (or trivial iterators), input iterators, output iterators, forward iterators, bidirectional iterators, and random-access iterators. • Iterators are generalizations of pointers. c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (16) Iterators as the clue Source: David R. Musser, et al., STL Tutorial and Reference Guide: C ++ Programming with the Standard Template Library, 2nd Edition, Addison-Wesley (2001) c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (17) Generic function accumulate Let n be a non-negative integer and xi a value of type V for i ∈ {0, 1, . . . , n − 1}. Assume that operator+ is defined for V. Function accumulate computes of type V. Pn−1 i=0 xi for any sequence of elements #include <iterator> // defines std::iterator_traits template <typename I> typename std::iterator_traits<I>::value_type accumulate(I p, I q) { typedef typename std::iterator_traits<I>::value_type V; V total = V(); while (p 6 ≡ q) { total += *p; ++p; } return total; } c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (18) Facilities available at compile time 1. Template parameters can be types. 2. Template parameters can be integral values (e.g. of type int, short, char, bool, or an enumeration type). 3. Template parameters can be templates, pointers, or functions. 4. sizeof can be evaluated at compile time. Surprisingly, the template mechanisms available in C++ can be exploited as a fully-fledged programming language. c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (19) Compile-time “assignments” typedefs are used to create new type aliases for other types. Member types can be used to propagate information between components at compile time. Compile-time mechanism Run-time mechanism template <typename size_type> class some_class { public: typedef size_type capacity_type; }; ... typedef unsigned int natural; typedef typename some_class<natural>:: capacity_type T; class some_class { public: some_class(std::string const size_type) : capacity_type(size_type) { } c Performance Engineering Laboratory std::string const capacity_type; }; ... std::string const natural = "unsigned int"; some_class object("natural"); std::string const T = object.capacity_type; Generic programming and library development, 29 April 2008 (20) Compile-time “variables” The “variables” of static C++ code are type names and integral constants. After the initialization the value cannot be changed. If you need a new type or value, you simply create a new type. Just as in functional programming, static C++ code uses symbolic names rather than true variables. That is, all the compile-time variables refer to true constants. c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (21) Compile-time “functions” B: a set of Boolean values, i.e. {false, true} S: a set of strings T : a set of type names Traits: T → T × T × . . . Static assertions: B → S Compile-time reflection: T → B Type functions: T → T Compile-time if: B → T c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (22) Generalized accumulate Generalize accumulate such that it computes tive operation ⊕. Ln−1 i=0 xi for any associa- template <typename I> typename std::iterator_traits<I>::value_type accumulate(I, I); Problem 1: Return type can be too small for the accumulated value. Problem 2: How to parameterize accumulate with ⊕? Problem 3: What to return if n = 0? That is, what is the zero value for ⊕? Problem 4: Function templates cannot have default template arguments. c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (23) Compile-time “structs” Traits are used to bundle different types together as their members. Traits represent natural additional properties of a template parameter. template <typename T> class accumulation_traits; template <> class accumulation_traits<bool> { public: typedef int return_type; }; template <> class accumulation_traits<char> { public: typedef int return_type; }; c Performance Engineering Laboratory template <> class accumulation_traits<short> { public: typedef int return_type; }; template <> class accumulation_traits<int> { public: typedef long return_type; }; template <> class accumulation_traits<float> { public: typedef double return_type; }; Generic programming and library development, 29 April 2008 (24) Design pattern: strategy Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy pattern lets the algorithm vary independently from clients that use it. Policies represent configurable behaviour for generic functions and types (often with some commonly used defaults). template < typename V, typename T = accumulation_traits<V> > class sum { public: template < typename V, typename T = accumulation_traits<V> > class zero { public: typedef typename T::return_type R; typedef typename T::return_type R; void accumulate(R& total, V const& v) { total += v; } }; c Performance Engineering Laboratory R initialize() { return R(); } }; Generic programming and library development, 29 April 2008 (25) Policy-based implementation of accumulate template < typename I, typename T = accumulation_traits<typename std::iterator_traits<I>::value_type>, typename S = sum<typename std::iterator_traits<I>::value_type, T>, typename Z = zero<typename std::iterator_traits<I>::value_type, T> > class accumulation { public: typedef typename T::return_type return_type; return_type accumulate(I p, I q) { Z initializer; S accumulator; return_type total = initializer.initialize(); while (p 6 ≡ q) { accumulator.accumulate(total, *p); ++p; } return total; } }; c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (26) Testing policy-based accumulate int main() { int numbers[] = {1, 2, 3, 4, 5}; unsigned int n = sizeof(numbers) / sizeof(numbers[0]); typedef accumulation<int*> A; A adder; A::return_type average = adder.accumulate(&numbers[0], &numbers[n]) / n; dynamic_assert(average ≡ A::return_type(3)); typedef accumulation_traits<int> T; typedef accumulation<int*, T, product<int, T>, one<int, T> > M; M multiplier; M::return_type product = multiplier.accumulate(&numbers[0], &numbers[n]); dynamic_assert(product ≡ (1 * 2 * 3 * 4 * 5)); return 0; } c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (27) Static assertions namespace cphstl { template <bool> class compile_time_checker { public: compile_time_checker(...) { } }; template <> class compile_time_checker<false> { }; #define static_assert(condition, message) { \ class ERROR_##message { \ }; \ typedef cphstl::compile_time_checker<(condition)> type; \ type temp = type(ERROR_##message()); \ (void) sizeof(temp); \ } } c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (28) Substitution failure is not an error (SFINAE) #include "assert.h++" typedef char RT1; typedef struct { char a[2]; } RT2; class yo_yo { public: typedef unsigned int string_length; }; template <typename T> RT1 test(typename T::string_length const*); template <typename T> RT2 test(...); #define has_member_type_string_length(T) \ (sizeof(test<T>(0)) ≡ 1) int main() { static_assert(has_member_type_string_length(yo_yo), testing_yo_yo); static_assert(has_member_type_string_length(int) ≡ false, testing_int); } c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (29) More SFINAE Or the trick can be encapsulated in a nice package: template<typename T> class IsClass { private: typedef char one; typedef struct{char a[2] } two; template <typename C> static one template <typename C> static two // size = 1 byte // size = 2 byte test(int C::*); // only classes test(...); // anything else public: enum {yes = sizeof(IsClass<T>::test<T>(0)) ≡ 1}; enum {no = !yes}; }; c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (30) SFINAE: usage Now we can write Or we might want to write pretty readable code if (IsClass<T>::yes) { // do something with class } else { // do something with non-class } template <typename T> bool is_class(T) { if (IsClass<T>::yes) { return true; } return false; } So we can simply write yo_yo dodah; if (is_class(dodah)) { ... c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (31) Curiously recurring template pattern (CRTP) Templates and inheritance can also be used together. template <typename Derived> class Base { public: ... }; template <typename T> class Child : public Base< Child<T> > { public: ... }; c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (32) CRTP: usage This can be useful for defining common interfaces without using an abstract base class. template <typename Derived> class Base { public: void f() { Derived& self = static_cast<Derived&>(*this); self.f(); } bool g(int count) const { Derived const& self = static_cast<Derived const&>(*this); return self.g(count); } }; Now the compiler ensures that class Child implements f and g. c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (33) CRTP: a problem? Not quite; what happens if template <typename Derived> class Base { public: void f() { Derived& self = static_cast<Derived&>(*this); self.f(); } }; class Child : public Base<Child> { public: }; • An infinite loop! • Oh, but shouldn’t the compiler tell us that we forgot to implement f on Child? c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (34) CRTP: workarounds Workaround 1: Use private inheritance. class Child : private Base<Child> { public: }; Workaround 2: Avoid name clashes. template<typename Derived> class Base { public: void f() { Derived& self = static_cast<Derived&>(*this); self.h(); } }; Workaround 3: Turn compiler warnings into errors. c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (35) Compile-time “if” #include "assert.h++" template <bool condition, typename Then, typename Else> class IF { public: typedef Then RET; }; //specialization for condition ≡ false template <typename Then, typename Else> class IF<false, Then, Else> { public: typedef Else RET; }; int main() { static_assert(sizeof(IF<(1 + 2 > 4), char, int>::RET) ≡ sizeof(int), testing_IF); IF<(1 + 2 > 4), char, int>::RET i; //the type of i is int! return 0; } c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (36) Compile-time recursion #include "assert.h++" template <int n> class Factorial { public: enum { RET = Factorial<n - 1>::RET * n }; }; // this template specialization terminates the recursion template <> class Factorial<0> { public: enum { RET = 1 }; }; int main() { static_assert(Factorial<7>::RET ≡ (1 * 2 * 3 * 4 * 5 * 6 * 7), testing_Factorial); return 0; } c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (37) Turing completeness A language is Turing complete if it provides a conditional and a looping construct. That is, the meta level of C++ can compute the same functions as a Turing machine. c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (38) Facilities missing • Varying number of template arguments is not (yet) supported (cf. the ellipsis construction for run-time functions). int printf(char const*, ...); • Types and integers can be manipulated at compile time, but not floats or strings. • Syntax for compile-time computations is terrible! c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (39) Research problem C++ is a combination of three languages: • macro language inherited from C, • run-time language, and • compile-time language. Design a language that • has the power of C++, • has a simple syntax, • is natural, • is minimal, and • can get equally many users as C++. c Performance Engineering Laboratory Generic programming and library development, 29 April 2008 (40)