License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright :
Frédéric Pennerath,
CentraleSupelec
Last modified : April 19, 2024 10:22
Link to the source : generic-programming.md
int
with RingElt
and we introduce identity()
Integer
and Polynomial
Solutions based on templates are generally more:
One could ask why GP and OOP are complementary if GP has only advantages over OOP. This is because the genericity of GP is limited to compilation time whereas OOP can bring polymorphism at runtime, thanks to virtual functions.
In weakly typed interpreted languages like Python
, you can even store values of any types in the same list (which is indeed a dynamic array or vector):
L = [ 1, 3.1415 ]
L.append('blah blah')
Even in strongly typed compiled languages like Java
, you can write:
import java.util.Vector;
Vector v = new Vector();
v.add(1); // Actually rewritten as v.add(new Integer(1));
v.add(true); // Actually rewritten as v.add(new Boolean(true));
You can even have strong typing thanks to generics that mimic syntactically C++ templates:
import java.util.Vector;
Vector v = new Vector<Integer>();
v.add(1); // Actually rewritten as v.add(new Integer(1));
v.add(true); // Fail at compilation
Yes, ok but despite appearances, this is not generic programming, this is object oriented programming.
Vector
is possible.void *
” that can point to any object.The real picture for Python, Java and most high-level languages look like the following object diagram:
In comparison, templates in C++ produce much more efficient implementations:
Let’s consider an example to illustrate the previous statements about the difference between GP and OOP. Consider Euclid’s algorithm that computes the greatest common divisor (GCD) of two integers.
Let’s recall its principle by manually running it to compute gcd(54,174)
:
174 = 54 x 3 + 12 -> gcd(174,54) = gcd(54,12)
54 = 12 x 4 + 6 -> gcd(54,12) = gcd(12,6)
12 = 6 x 2 + 0 -> gcd(12,6) = gcd(6,0) = 6
int gcd(int a, int b) {
while(b != 0) {
int r = a - b *(a/b); // Compute the remainder of a divided by b
a = b; // gcd(a,b) is equal to gcd(b,r) !
b = r;
}
return a; // When b = 0, gcd(a,b) is equal to a
}
While given for integers, this algorithm can be generalized to any commutative ring like the ones of polynomials, Gaussian integers, etc.
How can we rewrite the
gcd
function so that Euclid’s algorithm can be applied to any type of ring.
Two solutions exist:
How could we write a generic Euclid algorithm in Python or Java, based on OOP?
The main idea is to use inheritance and virtual methods.
One introduces an abstract class RingElt
and we leverage runtime polymorphism using virtual methods.
int
with RingElt
and we introduce identity()
class RingElt {
public:
static RingElt gcd(RingElt a, RingElt b) {
while(b != RingElt::identity()) {
RingElt r = a - b * (a/b);
a = b;
b = r;
}
return a;
}
static virtual const RingElt& identity() const = 0;
...
};
class Integer : public RingElt {
int i_;
static const Integer identity_;
public:
Integer(int i) : i_(i) {}
static const RingElt& identity() const override { return identity_; }
...
};
const Integer Integer::identity_ = Integer(0);
int
, etc) require to be encapsulated in a class (eg Integer
instead of int
) and thus an additional memory indirection.identity()
cannot be static
and virtual
at the same time.
class RingElt {
static RingElt gcd(RingElt a, RingElt b) {
while(b != b.identity()) {
RingElt r = a - b * (a/b);
a = b;
b = r;
}
return a;
}
virtual const RingElt& identity() const = 0;
...
};
class Integer : public RingElt {
...
const RingElt& identity() const override { return identity_; }
...
};
RingElt
is impossible and nonsense!RingElt
!Assignment of the underlying concrete objects (i.e. Integer
or Polynomial
) can be achieved using object cloning:
static RingElt* gcd(const RingElt& ra, const RingElt& rb) {
RingElt *a = ra.clone(), *b = rb.clone();
while(*b != b->identity()) {
RingElt* r = a->clone();
*r -= *b * (*a / *b);
a = b;
b = r;
}
return a;
}
class RingElt {
public:
static RingElt* gcd(const RingElt&, const RingElt&);
virtual const RingElt& identity() const = 0;
virtual RingElt* clone() const = 0;
};
class Integer : public RingElt {
int i_; ...
public:
Integer(int i) : i_(i) {}
...
virtual RingElt* clone() {
return new Integer(i_);
}
...
};
new
)static RingElt* gcd(const RingElt& ra, const RingElt& rb) {
RingElt *a = ra.clone(), *b = rb.clone();
while(*b != b->identity()) {
RingElt* r = a->clone();
*r -= *b * (*a / *b);
delete a;
a = b;
b = r;
}
delete b;
return a; // This "a" is not released to memory!
}
int main() {
Integer a(12), b(10);
Integer* p = std::static_cast<Integer*>(RingElt::gcd(a,b));
std::cout << "gcd(a,b) =" << p->i_ << std::endl;
delete p; // Should not forget to desallocate result
}
Integer*
). This is inelegant and dangerous as it breaks strong typing.delete
. This issue can be avoid by returning a shared pointer std::shared_ptr<RingElt>
.We delegate memory desallocation to shared pointers:
static std::shared_ptr<RingElt> gcd(const RingElt& ra, const RingElt& rb) {
std::shared_ptr<RingElt> a = ra.clone(), b = rb.clone();
while(*b != b->identity()) {
std::shared_ptr<RingElt> r = a->clone();
*r -= *b * (*a / *b);
a = b;
b = r;
}
return a;
}
class RingElt {
public:
static std::shared_ptr<RingElt> gcd(const RingElt&, const RingElt&);
virtual const RingElt& identity() const = 0;
virtual std::shared_ptr<RingElt> clone() const = 0;
};
class Integer : public RingElt {
int i_; ...
public:
Integer(int i) : i_(i) {}
...
virtual std::shared_ptr<RingElt> clone() const override {
return std::make_shared<Integer>(i_);
}
...
};
int main() {
Integer a(12), b(10);
std::shared_ptr<Integer> p = std::static_pointer_cast<Integer>(RingElt::gcd(a,b)); // Still a cast
std::cout << "gcd(a,b) =" << p->i_ << std::endl;
// No more delete here
}
std::static_pointer_cast<Integer>
). This can crash at runtime (i.e. segmentation faults) if the pointer does not point to an instance of Integer
.static std::shared_ptr<RingElt> gcd(const RingElt& ra, const RingElt& rb) {
std::shared_ptr<RingElt> a = ra.clone(), b = ra.clone();
while(*b !=(b->identity())) {
std::shared_ptr<RingElt> r = a->clone();
*r -= *b * (*a / *b);
a = b;
b = r;
}
return a;
}
class RingElt {
public:
static std::shared_ptr<RingElt> gcd(const RingElt&, const RingElt&);
virtual const RingElt& identity() const = 0;
virtual std::shared_ptr<RingElt> clone() const = 0;
virtual bool operator!=(const RingElt&) const = 0;
virtual RingElt& operator-=(const RingElt&) = 0;
virtual RingElt& operator*(const RingElt&) const = 0;
virtual RingElt& operator/(const RingElt&) const = 0;
};
RingElt& Integer::operator*(const RingElt& other) {
Integer* product = new Integer(i_ * static_cast<const Integer&>(other).i_);
return *product;
}
std::shared_ptr
but
std::shared_ptr<RingElt> Integer::operator*(const RingElt& other) {
return std::shared_ptr<RingElt>(new Integer(i_ * static_cast<const Integer&>(other).i_));
}
static std::shared_ptr<RingElt> gcd(const RingElt& ra, const RingElt& rb) {
std::shared_ptr<RingElt> a = ra.clone(), b = ra.clone();
while(*b != (b->identity())) {
std::shared_ptr<RingElt> r = a->clone();
*r -= *(*b * (*(*a / *b))); // Yucky!!!
a = b;
b = r;
}
return a;
}
To avoid these extra memory allocations, assignement operators can be used to store results on existing objects. But then the code loses in clarity:
static std::shared_ptr<RingElt> gcd(const RingElt& ra, const RingElt& rb) {
std::shared_ptr<RingElt> a = ra.clone(), b = ra.clone();
while(*b != b->identity()) {
std::shared_ptr<RingElt> r = a->clone();
*a /= *b;
*a *= *b;
*r -= *a;
a = b;
b = r;
}
return a;
}
class RingElt {
public:
static std::shared_ptr<RingElt> gcd(const RingElt&, const RingElt&);
virtual const RingElt& identity() const = 0;
virtual std::shared_ptr<RingElt> clone() const = 0;
virtual bool operator!=(const RingElt&) const = 0;
virtual RingElt& operator-=(const RingElt&) = 0;
virtual RingElt& operator*=(const RingElt&) = 0;
virtual RingElt& operator/=(const RingElt&) = 0;
};
Integer
and Polynomial
class Integer : public RingElt {
int i_; ...
public:
...
bool operator!=(const RingElt& elt) const {
const Integer& i = std::static_cast<Integer&>(elt); // Dangerous!!
return i_ != i.i_;
}
RingElt& operator*=(const RingElt& elt) {
const Integer& i = std::static_cast<const Integer&>(elt); // Dangerous!!
i_ *= i.i_;
return *this;
}
...
};
We have shown that OOP:
What about Generic Programming?
Let’s see how we can do the same using templates…
template<typename Ring>
Ring gcd(Ring a, Ring b) {
while(b != Ring()) {
Ring r = a - b * (a/b);
a = b;
b = r;
}
return a;
}
void main(int argc, char** argv) {
std::cout << "gcd(a,b) = " << gcd(12,10) << std::endl; // Identical to gcd<int>(12,10)
}
int
by Ring
.identity()
method: relies on the trick default numerical constructors (e.g. int()
) returns 0.gcd()
in main()
as an ordinary function without even noticing it is a template.It is not because a code runs as expected that work is over…
template<typename Ring>
Ring gcd(Ring a, Ring b) {
Ring e{}, r;
Ring *pa = &a, *pb = &b, *pr = &r;
while(*pb != e) {
*pr = *pa - *pb * (*pa / *pb);
Ring* old_pa = pa;
pa = pb; // Assignment of pointers instead of objects
pb = pr;
pr = old_pa;
}
return *pa;
}
Ring(const Ring&)
.Polynomial
.int
.template<typename Ring>
Ring gcd(Ring a, Ring b) {
Ring e{}, r;
while(b != e) {
r = a - b * (a/b); // Ring(&&) is called here
a = std::move(b); // and here
b = std::move(r);
}
return a;
}
Ring(&&)
saves an object copy in memorystd::move
forces to call Ring(&&)
int
and for Polynomial
Polynomial
:template<typename Scalar> class Polynomial : private std::vector<Scalar> {
public:
// Copy constructor
Polynomial(const Polynomial& other):
std::vector<Scalar>(other) {}
// Move constructor
Polynomial(Polynomial&& other) :
std::vector<Scalar>(std::move(other)) {}
...
};
Or even better:
template<typename Scalar> class Polynomial : private std::vector<Scalar> {
public:
template<typename P>
Polynomial(P&& other) :
std::vector<Scalar>(std::forward(other)) {}
...
};
Generic Programming conciliates:
The goal of a generic library based on templates is to be as generic as possible (i.e. usable in many contexts) without sacrificing performance. However genericity of some code has its own limits. How to specify to the user these limits, i.e. the requirements a generic algorithm or data structure expects from the types provided by the user in order to work correctly?
Let’s consider our example to determine to what extent function template gcd
is generic.
T
that can be passed to the argument Ring
?There are two distinct levels of requirements that types T
must satisfy:
template<typename Ring>
Ring gcd(Ring a, Ring b) {
Ring e{}, r;
while(b != e) {
r = a - b * (a/b);
a = std::move(b);
b = std::move(r);
}
return a;
}
Therefore, the following expressions must compile:
Expression | Syntactic requirement |
---|---|
Ring() |
Default constructor Ring() |
a = arg1 |
Operator Ring(const Ring&) |
a / b |
Operator Ring /(const Ring&) const |
a * b |
Operator Ring *(const Ring&) const |
a - b |
Operator Ring -(const Ring&) const |
a != b |
Operator bool !=(const Ring&) const |
Ring()
must return the identity element of \((Ring,+)\)./
verifies if \(y | x\) then, \((x / y) * y = x\).int f(Ring&)
such that for \(x \neq 0\), \(f(x) \geq 0\) and \(f(x - (x/y)*y) < f(x)\).T
satisfies a concept C
if all syntactic requirements of C
are satisfied by T
.T
models a concept C
if all syntactic and semantic requirements of C
are satisfied by T
. In that case, T
is called a model for concept C
.Here is an example that illustrates how to use concept to specify the requirements of algorithm gcd
:
// Header file containing predefined concepts
#include <concepts>
// Definition of concept Ring
template<typename T> concept Ring =
std::default_initializable<T> && // Reuse here predefined elementary concepts
std::copy_constructible<T> &&
std::move_constructible<T> &&
std::destructible<T> &&
std::equality_comparable<T> &&
requires(T a, T b) { // Definitions of specific constraints
{ a + b } -> std::convertible_to<T>; // Addition must be defined between two instances of T
{ a - b } -> std::convertible_to<T>; // and return a value implicitely convertible to a value of type T
{ a * b } -> std::convertible_to<T>;
{ a / b } -> std::convertible_to<T>;
};
// Usage of concept Ring : T must satisfy constraints of concept Ring
template<Ring T>
T gcd(T a, T b) {
T e{}, r;
while(b != e) {
r = a - b * (a/b);
a = std::move(b);
b = std::move(r);
}
return a;
}
A
can include all requirements of algorithm B
\(\rightarrow\) need to organize concepts in an hierarchy:
A
is a subconcept of concept B
if every requirement of B
is also a requirement of A
.A
is also a model of B
.Propose a generic algorithm that computes the weighted barycenter of a set of elements. These elements will be passed to the algorithm using a range of points [begin, end[
. The same for weights. One will test the algorithm using provided file test-barycenter.cpp
. This programm will check the validity and the generic character of yoru algorithm on elements of type double
but also on the class templates Vector
and Matrix
that you have developped (and that you will complete if necessary). Formalize the requirements of your algorithm by introducing a concept.