Generic programming & library development

advertisement
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)
Download