CPP Sci Comp
CPP Sci Comp
CPP Sci Comp
Ronald Kriemann
MPI MIS Leipzig
MIS
2008
1/316
Chapters
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Introduction
Variables and Datatypes
Arithmetic Operators
Type Casting
Blocks and Scope
Control Structures
Functions
Arrays and Dynamic Memory
Advanced Datatypes
Modules and Namespaces
Classes
Generic Programming
Error Handling
Standard Template Library
Class Inheritance
Appendix
2/316
Introduction
3/316
Why C++?
Why not Matlab?
Matlab is a high level language, e.g. provides many
functions/algorithms allowing rapid developement.
But Matlab is limited to dense and (to some degree) sparse
LAPACK.
Fortran 77 quite dated in terms of language features.
Recent updates of Fortran (90, 95, 2000) modernised the
4/316
Why C++?
So, Why C++ (and not C) ?
C is a subset of C++.
C++ provides many features which make programming easier.
C++ can be as fast as C (and sometimes faster).
C++ (like C) can use all software libraries written in Fortran or
C.
Many new software libraries are written in C++.
5/316
Hello World
Like every programming course, we start with something simple:
#include <iostream>
using namespace std;
int
main ( int argc, char ** argv )
{
// print welcome message
cout << "Hello World" << endl;
return 0;
}
6/316
Hello World
C++ is a compiler based language, i.e. one has to translate the
source code of the program into a machine executable format
using another program, called the compiler.
Source code files, or just source files, typically have a filename
suffix like .cc, .C or .cpp.
There are many different C++ compilers available even for one
operating system. On Linux, the GNU Compiler Collection
provides the g++ compiler. Alternatively, Intel offers another
compiler named icpc.
As an example, to compile the source file hello.cc for Hello
World into an executable binary using the GCC compiler, youll
have to enter
g++ -o hello hello.cc
and afterwards run the program via ./hello.
C++ for Scientific Computing
7/316
8/316
9/316
10/316
11/316
12/316
x + 1 108 = x
For any floating point type, the smallest number , such that
1 + 6= 1 is known as the machine precision:
for float: 1.2 107 ,
for double: 2.2 1016 .
13/316
14/316
15/316
n;
i, j, k;
pi, Pi, PI;
1_over_pi;
_1_over_pi;
// ERROR
// Ok
n
i, j, k;
pi
_1_minus_pi
max
= 10;
= 3.1415926;
= 1.0 - pi;
= 1.8e308;
16/316
n
= 10;
i, j, k;
pi;
_1_minus_pi = 1.0 - pi;
max
= 1.8e308;
17/316
Type Modifiers
Each type can be modified with one of the following modifiers:
const: the value of the variable is fixed,
static: the variable is declared only once in the whole
n
i, j, k;
pi
_1_minus_pi
max
= 10;
= 3.1415926;
= 1.0 - pi;
= 1.8e308;
18/316
19/316
20/316
=
=
=
=
n = 5;
p = & n;
m1, m2;
n + 3;
*p + 3;
6;
n + 4;
// m2 equal to m1
// this changes the value of n to 6!
// evaluates to 10!
q = p;
n = 2;
m1 = *p + *q
21/316
p = NULL;
22/316
n = 5;
r = n;
m;
m = r + 3;
r = m;
m = 0;
// m == n + 3
// r still points to n and n == m
// r and n are unchanged
And you can not change the address where a reference points to:
int &
r = s;
s = m;
// r still points to n and n == m (== 0)
23/316
*
*
* const
* const
pn1;
pn2;
pn3;
pn4;
//
//
//
//
n
m
int *
const int *
int * const
pn1 = & n; // Ok
pn2 = & n; // ERROR
pn3 = & n; // Ok
= 0;
= 1;
24/316
Arithmetic Operators
25/316
+
*
/
y;
y;
y;
y;
Remark
No operator for power, like ^ in Matlab.
26/316
x = x - y;
x = x / y;
// for integers
is the same as
x += y;
x *= y;
x %= y;
x -= y;
x /= y;
// for integers
27/316
n = 0;
++n; // same as n = n + 1 or n += 1
n++;
--n; // same as n = n - 1 or n -= 1
n--;
n = 5, m = 5 * n++;
n = 5, m = 5 * ++n;
28/316
n1 = 3, n2 = 4;
n3;
n3 = 2 * n1 - n2 + n1 / 2;
n2 *= 6;
n1 -= 8;
const double
double
double
29/316
30/316
>
<
>=
<=
==
!=
y;
y;
y;
y;
y;
y;
//
//
//
//
//
//
bigger than
less than
bigger or equal
less or equal
equal
unequal
Logic
Logical operators and, or and not for boolean values:
b1 && b2;
b1 || b2;
! b1;
// and
// or
// not
31/316
x = 2;
y = 4;
z = 4;
b;
// z == 4 is not tested
b = ( x == 2 && y == 3 && z == 4 );
// only x == 2 is tested
b = ( x == 2 || y == 3 || z == 4 );
// correct, since x != 0 in "y/x"
b = ( x != 0 && y/x > 1 );
32/316
f1
f2
b;
= sqrt( 2.0 );
= f1 * f1;
b = ( f2 == 2.0 );
const double
// unsafe
eps = 1e-14;
// safe
// even better
33/316
n1 = 2 + 3 * 4 % 2 - 5;
n2 = 4;
b = n1 >= 4 && n2 != 3 || n1 < n2;
n1 = (2 + ((3 * 4) % 2) - 5);
n2 = 4;
b = (((n1 >= 4) && (n2 != 3)) || (n1 < n2));
34/316
n1 = 1 + 2 + 3 + 4;
n2 = 1 * 2 * 3 * 4;
n3, n4 , n5;
n3 = n4 = n5 = 0;
is the same as
int
int
int
n1 = (((1 + 2) + 3) + 4);
n2 = (((1 * 2) * 3) * 4);
n3, n4 , n5;
35/316
Associativity
left
right
lowest
left
left
left
left
left
left
right
Operators
()
++, --, - (unary), !, & (address),
* (dereference)
* (multiplication), /, %
+, >, <, >=, <=
==, !=
&&
||
=, +=, -=, *=, /=, %=
36/316
Precedence of Operators
Usage of Parentheses
Example:
int
int
bool
n1 = 4;
n2 = 2 + 5 * n1 / 3 - 5;
b = n2 >= 4 && n1 != 3 || n2 < n1;
vs.
int
int
bool
n1 = 4;
n2 = 2 + (5 * n1) / 3 - 5;
b = ( n2 >= 4 ) && (( n1 != 3 ) ||
( n2 < n1 ));
37/316
Type Casting
38/316
Type Casting
Implicit Type Conversion
Up to now, we have only had expressions with either integer or
floating point types. Often, these types are mixed:
const double
double
pi = 3.14159265358979323846;
f = 1 / ( 2 * pi );
pi = 3.14159265358979323846;
n = 2 * pi;
39/316
Type Casting
Explicit Type Conversion
Explicit conversion between data types is performed via
typename( value )
Examples:
const float
int
bool
pi = float(3.14159265358979323846);
n = 2 * int(pi);
b = bool(n);
Remark
A non-zero value is interpreted as true, a zero value as
false.
The old C syntax (typename) value, e.g.
int
n = 2 * (int) pi;
40/316
Type Casting
Problems while Casting
Problem 1: Different ranges:
int
unsigned int
n1 = 5 - 6;
n2 = n1;
// underflow
pi = 3.14159265358979323846;
f1 = 2.1;
f2 = 4 * f1 / pi;
41/316
Type Casting
Cast Operators
C++ defines four cast operators for various purposes:
const casthTi(v) : remove const qualifier of a variable v,
static casthTi(v) : classic, compile time type conversion with
type checking
reinterpret casthTi(v) : type conversion with correct handling
checking
Since these operators are usually used in connection with classes,
we will come back to them later.
42/316
43/316
Remark
The trivial block is defined by a single statement.
Blocks can be arbitrarily nested or otherwise put together:
{ // block 1
{ // subblock 1.1
}
{ // subblock 1.2
{ // sub subblock 1.2.1
}
}
}
C++ for Scientific Computing
44/316
// ERROR
45/316
n1 declared
n1, n2 declared
n4 = 4;
{ // block 1.2:
int n3 = 3;
}
46/316
// n1 == 1
// n1 == 2
// n1 == 1
{ // block 1.2
int n1 = 3;
}
{ // block 1.3
n1 = 4;
}
// n1 == 3
// n1 == 4 !!!
}
47/316
// evaluates to m = 3
}
{ // block 1.2
int n2 = 3;
m = n1 + n2;
// evaluates to m = 4
}
}
48/316
49/316
n = 0;
{
double f = 1.2;
...
}
}
For static variables, the memory will never be released and only
allocated once per program run:
{
}
C++ for Scientific Computing
Control Structures
51/316
Control Structures
Conditional Structure
A condition is defined by
if ( hconditioni ) hblocki
where hblocki is executed if hconditioni is true or
if ( hconditioni ) hif blocki
else helse blocki
where additionally helse blocki is executed if hconditioni is false,
e.g.
int
n = 1;
if ( n > 0 )
{
n = n / n;
}
i f ( n < 0 ) n += 5;
else
n -= 6;
C++ for Scientific Computing
52/316
Control Structures
C++ supports three different loops: for loops, while loops and
do-while loops.
For Loops
for ( hstart stmt.i ; hloop conditioni ; hloop stmt.i )
hblocki
The start statement is executed before the loop is entered. Before
each iteration, the loop condition is evaluated. If it is false, the
loop is finished. After each iteration, the loop statement is
executed.
Example for factorial:
int
n = 1;
53/316
Control Structures
While Loops
while ( hloop conditioni )
hblocki
The loop iterates until the loop condition is no longer true, i.e.
evaluates to false. The condition is checked before each iteration.
Example for factorial:
int
int
n = 1;
i = 1;
while ( i < 10 )
{
n *= i;
++i;
}
54/316
Control Structures
Do-While Loops
do
hblocki
while ( hloop conditioni );
The loop condition is tested after each iteration. Hence, the block
is at least executed once.
Example for factorial:
int
int
n = 1;
i = 1;
do
{
n *= i;
++i;
} while ( i < 10 );
55/316
Control Structures
Breaking out of Loops
To finish the current loop immediately, e.g. without first testing
the loop condition, the keyword break can be used:
int
n = 1;
56/316
Control Structures
Breaking out of Loops
Only the current loop is finished, not all enclosing loops:
for ( int j = 1; j < 20; ++j )
{
int n = 1;
for ( int i = 1; i < j; ++i )
{
i f ( n > 21474836 )
// break here
break;
n = n * i;
}
cout << n << endl;
57/316
Control Structures
Finish current Iteration
To immediately finish the current iteration of a loop use the
keyword continue:
int
n2 = 0;
58/316
Control Structures
Selective Statement
To directly switch between several cases, the switch structure can
be used:
switch ( hvaluei ) {
case hCASE 1i : hstatementsi; break;
case hCASE 2i : hstatementsi; break;
..
.
case hCASE ni : hstatementsi; break;
default : hstatementsi
}
The type of hvaluei must be some integral typ, i.e. it can be
mapped to an integer type. Hence, floating point or more
advanced types (later) are not possible.
59/316
Control Structures
Selective Statement
Example for a switch statement:
unsigned int
unsigned int
switch ( i )
{
case 0 : n =
case 1 : n =
case 2 : n =
case 3 : n =
case 4 : n =
default : n =
}
i = 3;
n;
1;
1;
2;
6;
24;
0;
break;
break;
break;
break;
break;
break;
60/316
Control Structures
Selective Statement
If break is missing after the statements for a specific case, the
statements of the next case are also executed:
unsigned int
unsigned int
switch ( i )
{
case 0 :
case 1 : n =
case 2 : n =
case 3 : n =
case 4 : n =
default : n =
}
i = 3;
n;
1;
2;
6;
24;
0;
61/316
Control Structures
Selective Statement
A switch statement can be implemented via if and else:
unsigned int
unsigned int
if
else
else
else
else
else
if
if
if
if
(
(
(
(
(
i
i
i
i
i
i = 3;
n;
==
==
==
==
==
0
1
2
3
4
)
)
)
)
)
{
{
{
{
{
{
n
n
n
n
n
n
=
=
=
=
=
=
1;
1;
2;
6;
24;
0;
}
}
}
}
}
}
// default statement
Remark
Using switch is faster than if-else, among other things
since less comparisons are performed!
62/316
Functions
63/316
Functions
Function Definition
The definition of a function in C++ follows
hreturn typei
hfunction namei ( hargument listi )
hblocki
When a function does not return a result, e.g. a procedure, then
the return type is void.
To return a value from a function, C++ provides the keyword
return.
double
square ( const double x )
{
return x*x;
}
64/316
Functions
Function Call
A function is called by
hfunction namei ( hargument1i, hargument2i, . . . );
Example:
double y = square( 4.3 );
y ) { return y*y; }
return square( x ) * x;
}
65/316
Functions
Function Examples
Previous computation of factorial in functional form:
int
factorial ( const int
{
int f = 1;
n )
66/316
Functions
Function Examples (Cont.)
Power function with positive integer exponents:
double
power ( const double x, const unsigned int n )
{
switch ( n )
{
case 0 : return 1;
case 1 : return x;
case 2 : return square( x );
default:
{
double f = x;
for ( int i = 0; i < n; i++ ) f *= x;
return f;
}
}
}
67/316
Functions
Main Function
The main function is the first function called by the operating
system in your program. Every program must have exactly one
main function.
In principle, only code in main and functions called directly or
indirectly from main will be executed.
The main function may be implemented without arguments and
has a return type of int:
int
main ()
{
... // actual program code
return 0;
}
68/316
Functions
Call by Value
In previous examples, only the value of a variable (or constant) is
used as the argument of a function, e.g. changing the value of the
argument does not change the value of the original variable:
int
f ( int m )
{
m = 4;
return m;
}
int
int
m = 5;
n = f( m ); // m is unchanged by f
69/316
Functions
Call by Reference
If the original variable should be changed in a function, a pointer
or reference to this variable has to be supplied:
int
f ( int & m )
{
m = 4;
return m;
}
int
int
// reference argument
// changing m, changes the variable pointed to
m = 5;
n = f( m ); // m is changed by f to 4
70/316
Functions
Call by Reference
The same function with pointers:
int
f ( int * m )
{
*m = 4;
return *m;
}
int
int
// reference argument
// changing m, changes the variable pointed to
m = 5;
n = f( & m ); // m is changed by f to 4
71/316
Functions
Call by Reference
When using references to constant variables, the value can not be
changed:
int
f ( const int & m )
{
m = 4;
// ERROR: m is constant
return m;
}
72/316
Functions
Call by Reference
Example for multiple return values
void
min_max ( const int n1, const int n2, const int n3,
int & min, int & max )
{
i f ( n1 < n2 )
i f ( n1 < n3 )
{
min = n1;
i f ( n2 < n3 ) max = n3;
else
max = n2;
}
else
{
min = n3;
max = n2;
else
}
...
73/316
Functions
Recursion
Calling the same function from inside the function body, e.g. a
recursive function call, is allowed in C++:
unsigned int
factorial ( const unsigned int n )
{
i f ( n <= 1 )
return 1;
else
return n * factorial( n-1 );
}
Remark
The recursion depth, i.e. the number of recursive calls, is
limited by the size of the stack, a special part of the
memory. In practise however, this should be of no
concern.
C++ for Scientific Computing
74/316
Functions
Recursion
It is also possible to perform recursive calls multiple times in a
function:
unsigned int
fibonacci ( const unsigned int
{
unsigned int retval;
n )
i f ( n <= 1 ) retval = n;
else
retval = fibonacci( n-1 ) +
fibonacci( n-2 );
return retval;
}
Remark
Remember, that variables belong to a specific block and
each function call has its own block. Therefore,
variables, e.g. retval, are specific to a specific function
call.
C++ for Scientific Computing
75/316
Functions
Function Naming
A function in C++ is identified by its name and the number and
type of its arguments. Hence, the same name can be used for
different argument types:
int
float
double
76/316
Functions
Function Naming
If only the return type is different between functions, they are
identified as equal:
float f ( int x ) { ... }
double f ( int x ) { ... }
77/316
Functions
Default Arguments
Arguments for a function can have default arguments, which then
can be omitted at calling the function:
void
f ( int n, int m = 10 )
{
// ...
}
{
f( 5 );
// equivalent to f( 5, 10 )
f( 5, 8 );
}
Only limitation: after the first default value, all arguments must
have default values:
void g1 ( int n, int m = 10, int k );
// ERROR
void g2 ( int n, int m = 10, int k = 20 ); // Ok
78/316
Functions
Default Arguments and Function Names
Two functions with the same name must differ by their arguments
without default values:
void f ( int n1, int n2, int n3 = 1 ) { ... }
void f ( int n1, int n2 )
{ ... }
...
{
f( 1, 2, 3 );
f( 1, 2 );
79/316
Functions
Function Name Scope
A function can only be called, if it was previously implemented:
void f ( int x )
{
g( x );
}
void g ( int y )
{
f( y );
}
// forward declaration
80/316
Functions
Function Name Scope
Of course, every function with a forward declaration has to be
implemented eventually:
void g ( int y );
// forward declaration
void f ( int x )
{
g();
}
...
void g ( int y )
{
f( y );
}
// implementation
81/316
Functions
Inline Functions
Calling a function involves some overhead. For small functions,
this overhead might exceed the actual computation:
double
{
double
f = 0;
...
82/316
Functions
Inline Functions
Replacing the function call by the function body is called inlining.
To help the compiler with such decisions, functions can be marked
to be inlined by the keyword inline:
i n l i n e double
square ( const double x )
{
return x*x;
}
83/316
Functions
Function Pointers
A function, like a variable, is stored somewhere in the memory and
therefore, also has an address. Hence, a pointer can be aquired for
it. For a function
hreturn typei hfunction namei ( hargument listi );
a pointer is defined by
hreturn typei ( * hvariable namei ) ( hargument listi );
Example:
int f ( const int
{
n, int &
r );
pf = f;
}
84/316
Functions
Function Pointers
A variable holding the address of a function can be used as a
function by itself:
int
n = 0;
pf = f;
pf( 2, n );
// pf holds address to f
// call to f
f1 ( const double x )
double
int main ()
{
f2( f1, 2.0 );
}
{ return
x*x; }
85/316
Functions
Function Pointers
Example: apply Simpson rule to various functions
double
simpson_quad ( const double a, const double b,
double ( * func ) ( const double ) )
{
return (b-a) / 6.0 * ( func(a) +
4 * func( (a+b) / 2.0 ) +
func(b) );
}
double f1
double f2
( const double
( const double
x ) { return x*x; }
x ) { return x*x*x; }
int main ()
{
cout << simpson_quad( -1, 2, f1 ) << endl;
cout << simpson_quad( -1, 2, f2 ) << endl;
}
86/316
Functions
Functions and Minimal Evaluation
As discussed, C++ uses minimal evaluation when looking at logical
expressions, e.g. only evalutates until results is known. If functions
are used in the expressions, it can imply, that they are not called at
all:
double f ( const double
x ) { ... }
...
// f is not called if x >= 0.0
i f (( x < 0.0 ) && ( f( x ) > 0.0 ))
{
...
}
87/316
Functions
Functions and static Variables
In contrast to standard variables in a function, which are specific
to a specific function call, for static variables in all function calls
the same instance, e.g. memory position, is referenced:
double
f ( const double x, long & cnt )
{
static long counter = 0;
// allocated and initialised
// once per program
cnt = ++counter;
return 2.0*x*x - x;
}
int main ()
{
long cnt = 0;
for ( double x = -10; x <= 10.0; x += 0.1 )
f( x, cnt );
cout << cnt << endl;
}
C++ for Scientific Computing
88/316
89/316
n[ 5 ];
f[ 10 ];
len = 32;
str[ len ];
Arrays can also be preinitialised. In that case, the array size can be
omitted:
int
int
n1[5] = { 0, 1, 2, 3, 4, 5 };
n2[] = { 3, 2, 1, 0 };
// automatically size of 4
90/316
f[5];
i;
f[0] = -1.0;
f[1] = 3.0;
f[4] = f[1] * 42.0;
i
= 3;
f[i] = f[0] + 8.0;
In C++, indices are counted from zero. The valid index range is
therefore:
[0, . . . , array size 1]
for ( int i = 0; i < 5; i++ )
f[i] = 2*i;
91/316
f[5];
// Ok
// Bug
92/316
}
void
{
Remark
Arrays can be used as function arguments like all basic
datatypes. But not as function return types!
C++ for Scientific Computing
93/316
M[3][3];
T3[10][10][10];
T4[100][20][50];
94/316
M[3][3],
x[3],
y[3] )
< 3; i++ )
95/316
// yields n[0]
// yields n[1]
// yields n[4]
96/316
97/316
s
n
v
f
=
=
=
=
=> delete
Remark
The size parameter to new does not need to be a
constant.
C++ for Scientific Computing
98/316
99/316
v;
...
v[100] = 2.0;
Again, the last instruction will be executed and will only result in
an immediate error, if the memory is no longer part of the program.
Coding Principle No. 15
After calling delete, reset the pointer value to NULL.
C++ for Scientific Computing
100/316
101/316
102/316
102/316
Memory (array)
n[5] = { 2, 3, 5, 7, 11 };
p1
= & n[2];
p2
= & p1;
p2
11
p1
*
*
*
*
n1[4],
p1
=
p2
=
p3
=
p4
=
p
1
= n1
= n2
= n3
= n4
p1
p2
p3
p4
103/316
*
*
*
*
p1
p2
p3
p4
=
=
=
=
new
new
new
new
p1;
p2;
p3;
p4;
p[0][0]
p[0][1]
...
p[2][2]
p[2][3]
p[3][0]
...
int[4];
int[4];
int[4];
int[4];
= new int*[4];
int ** p
p[0]
p[1]
p[2]
p[3]
=
=
=
=
p
1
= n1
= n2
= n3
= n4
p1
p2
p3
p4
= 1;
= 2;
= 7;
= 6;
= 5;
104/316
= n2
2
= n3
6
= n4
8
global Memory
p
int
* p = new int[4*4];
p[ 2 * 4 + 1 ] = 8;
p[ 0 * 4 + 2 ] = 3;
// p[2][1]
// p[0][2]
105/316
Fortran, Matlab
rowwise
columnwise
i m + j,
column-wise:
j n + i.
106/316
107/316
108/316
mapping (row-wise),
should provide functions for all standard operations, e.g.
i )
i n l i n e double *
matrix_init ( const unsigned n, const unsigned
{
return new double[ n * m ];
}
m )
109/316
double
dot ( const unsigned
{
double d = 0.0;
110/316
m,
void
add ( const unsigned n, const unsigned m,
const double f, const double * A, double * M );
i n l i n e double
normF ( const unsigned n, const unsigned m,
double * M )
{ return norm2( n*m, M ); } // use vector based norm2
111/316
Remark
Compute dot product in local variable to minimize
memory accesses.
C++ for Scientific Computing
112/316
113/316
// fill matrix M
114/316
or
char * str
str[] = { S, t, r, i, n, g, \0 };
str[] = "String";
// array initialisation
115/316
116/316
117/316
Result
a new line
a tab
a carriage return
a backspace
single quote 0
double quote
backslash \
Examples:
cout
cout
cout
cout
<<
<<
<<
<<
"First \t Second"
"line1 \n line2"
"special \"word\""
"set1 \\ set2"
<<
<<
<<
<<
endl;
endl;
endl;
endl;
118/316
Advanced Datatypes
119/316
Advanced Datatypes
Type Definition
Often you do not always want to care about the actual datatype
used in your program, e.g. if it is float or double or if strings are
char *, but instead give the types more reasonable names, e.g.
real or string. In C++ you can do this via typedef:
typedef hdata typei hnamei;
Afterwards, hnamei can be used like any other datatype:
typedef
typedef
typedef
double
char *
real_t **
const string_t
matrix_t
real_t
real_t;
string_t;
matrix_t;
// pointers of pointers
str = "String";
A
= new real_t*[ 10 ];
f
= real_t( 3.1415926 );
120/316
Advanced Datatypes
Type Definition
Remark
A real t datatype allows you to easily change between
float and double in your program.
To simplify the destinction between variables and datatypes, the
following is strongly advised:
Coding Principle No. 18
Follow a strict convention in naming new types, e.g. with
special prefix or suffix.
121/316
Advanced Datatypes
Predefined Types
The C++ library and the operating system usually define some
abbreviations for often used types, e.g.
uint: unsigned integer, sometimes special versions uint8,
available)
122/316
Advanced Datatypes
Records
Working with vectors and matrices always involved several
variables, e.g. the size and the arrays. That results in many
arguments to functions and hence, to possible errors. It would be
much better to store all associated data together. That is done
with records:
struct hrecord namei {
hdatatype 1i hname 1i;
..
.
hdatatype ni hname ni;
};
By defining a struct, also a new type named hrecord namei is
defined.
123/316
Advanced Datatypes
Records
Example:
struct vector_t {
size_t
size;
real_t * coeffs;
};
struct matrix_t {
size_t
nrows, ncolumns;
real_t * coeffs;
};
void
mul_vec ( const real_t
const matrix_t &
const vector_t &
vector_t &
struct triangle_t {
int
vtx_idx[3];
real_t normal[3];
real_t area;
};
C++ for Scientific Computing
alpha,
A,
x,
y );
124/316
Advanced Datatypes
Access Records
The individual variables in a record are accessed via ., e.g.:
vector_t
x;
x.size
= 10;
x.coeffs = new real_t[ x.size ];
x = new vector_t;
x->size = 10;
x->data = new real_t[ x->size ];
cout << (*x).size << endl; // alternative
125/316
Advanced Datatypes
Access Records
In case of a reference, e.g. vector t &, the standard access has to
be used, e.g. via .:
vector_t
x;
x.size
= 10;
x.coeffs = new real_t[ x.size ];
vector_t & y = x;
cout << y.size << endl;
cout << y.coeffs[5] << endl;
126/316
Advanced Datatypes
Records and Functions
Records can be supplied as normal function arguments, either as
call-by-value or call-by-reference:
double dot ( const vector_t
void
fill ( const real_t
void
add ( const real_t
vector_t &
x, const vector_t
f, vector_t &
f, const vector_t &
y );
y );
y );
x,
127/316
Advanced Datatypes
Records and Functions
In such cases, call-by-reference with a const argument is necessary
to avoid this overhead:
double quadrature ( const quadrule_t & rule,
double ( * func ) ( const double x ) );
128/316
Advanced Datatypes
Application: BLAS (Version 2)
Modified BLAS function set using the previous record types for
vectors and matrices:
i n l i n e vector_t *
vector_init ( const unsigned i )
{
vector_t * v = new vector_t;
v->size = i;
v->coeffs = new real_t[ i ];
for ( unsigned i = 0; i < n; i++ )
v->coeffs[i] = 0.0;
// RAII
return v;
}
i n l i n e matrix_t *
matrix_init ( const unsigned
{ ... }
n, const unsigned
m )
129/316
Advanced Datatypes
Application: BLAS (Version 2)
// vector functions
void fill ( const double f, vector_t & x );
void scale ( const double f, vector_t & x );
void add
( const double f, const vector_t & x, vector_t & y )
{
for ( unsigned i = 0; i < n; i++ )
y.coeffs[i] += f * x.coeffs[i];
}
double
dot
( const vector_t & x, const vector_t & y );
i n l i n e double norm2 ( const vector_t & x )
{ return sqrt( dot( x, x ) ); }
// matrix functions
void fill ( const double f, matrix_t & M );
void scale ( const double f, matrix_t & M );
void add
( const double f, const matrix_t & A, matrix_t & M );
i n l i n e double
normF ( double & M )
{ ... } // can not use vector based norm2!
C++ for Scientific Computing
130/316
Advanced Datatypes
Application: BLAS (Version 2)
void
mul_vec ( const double
const matrix_t &
const vector_t &
vector_t &
{
for ( unsigned i = 0; i
{
double f = 0.0;
alpha,
M,
x,
y )
< M.nrows; i++ )
alpha,
A,
B,
C );
131/316
Advanced Datatypes
Application: BLAS (Version 2)
matrix_t * M = matrix_init( 10, 10 );
vector_t * x = vector_init( 10 );
vector_t * y = vector_init( 10 );
fill( 1.0, x );
fill( 0.0, y );
...
// fill matrix M
132/316
Advanced Datatypes
Recursive Records
Records can have variables of its own type in the form of a pointer.
That way, recursive structures can be defined, e.g. a binary tree:
struct node_t {
int
val;
node_t * son1, * son2;
};
node_t
node_t
node_t
root;
son1, son2;
son11, son12, son21, son22;
root
son1
son11
son2
son12
son21
son22
133/316
Advanced Datatypes
Recursive Records
Insert new value in binary tree:
void
insert ( const node_t & root, const int val )
{
i f ( val < root.val )
{
i f ( root.son1 != NULL )
insert( * root.son1, val );
else
{
root.son1
= new node_t;
root.son1->val = val;
root.son1->son1 = NULL;
root.son1->son2 = NULL;
}
}
else
{
i f ( root.son2 != NULL )
insert( * root.son2, val );
else
...
}
}
C++ for Scientific Computing
134/316
Advanced Datatypes
Recursive Records
Example for insertion:
int
node_t
values[7] = { 5, 3, 1, 4, 8, 6, 9 };
root;
yields:
5
3
1
8
4
135/316
Advanced Datatypes
Recursive Records
Looking for value in binary tree:
bool
is_in ( const node_t & root, const int val )
{
i f ( root.val == val )
return true;
return is_in( * root.son1, val ) ||
is_in( * root.son2, val );
}
...
cout << is_in( root, 6 ) endl;
cout << is_in( root, 7 ) endl;
// yields true
// yields false
136/316
Advanced Datatypes
Arrays of Records
Like any other datatype, records can also be allocated in the form
of an array:
struct coord_t {
real_t x, y, z;
};
coord_t
coordinates[ 10 ];
137/316
Advanced Datatypes
Arrays of Records
The access to record variables then comes after addressing the
array entry:
for ( unsigned i = 0; i < 10; i++ )
{
coordinates[i].x = cos( real_t(i) * 36.0 * pi / 180.0 );
coordinates[i].y = sin( real_t(i) * 36.0 * pi / 180.0 );
coordinates[i].z = real_t(i) / 10.0;
}
138/316
Advanced Datatypes
Record Application: Sparse Matrices
We only want to store nonzero entries in a sparse matrix. For this,
each entry is stored in a record type, containing the column index
and the coefficient:
struct entry_t
unsigned
real_t
entry_t *
};
{
column;
coeff;
next;
column
column
coeff
coeff
coeff
next
next
next
NULL
139/316
Advanced Datatypes
Record Application: Sparse Matrices
A sparse matrix is then allocated as an array of entry lists per row:
struct sparsematrix_t {
unsigned
nrows, ncolumns;
entry_t * entries;
};
sparsematrix_t
nrows
ncolumns
entries
entry_t
entry_t
entry_t
entry_t
entry_t
entry_t
entry_t
entry_t
entry_t
140/316
Advanced Datatypes
Record Application: Sparse Matrices
As an example, consider the matrix
row
1
3
0
2
1
1
4 1 1
2
1
3
3
sparsematrix_t
entry_t *
S;
entry;
S.nrows = 4; S.ncolumns = 4;
S.entries = new entry_t[4];
// first row
entry = & S.entry[0];
entry->column = 0; entry->coeff = 1.0; entry->next = new entry_t;
entry = entry->next;
entry->column = 2; entry->coeff = 3.0; entry->next = NULL;
...
C++ for Scientific Computing
141/316
Advanced Datatypes
Record Application: Sparse Matrices
As an example, consider the matrix
row
1
3
0
2
1
1
4 1 1
2
1
3
1
0
sparsematrix_t
entry_t *
S;
entry;
S.nrows = 4; S.ncolumns = 4;
S.entries = new entry_t[4];
// first row
entry = & S.entry[0];
entry->column = 0; entry->coeff = 1.0; entry->next = new entry_t;
entry = entry->next;
entry->column = 2; entry->coeff = 3.0; entry->next = NULL;
...
C++ for Scientific Computing
141/316
Advanced Datatypes
Record Application: Sparse Matrices
As an example, consider the matrix
row
1
3
0
2
1
1
4 1 1
2
1
3
1
0
NULL
sparsematrix_t
entry_t *
S;
entry;
S.nrows = 4; S.ncolumns = 4;
S.entries = new entry_t[4];
// first row
entry = & S.entry[0];
entry->column = 0; entry->coeff = 1.0; entry->next = new entry_t;
entry = entry->next;
entry->column = 2; entry->coeff = 3.0; entry->next = NULL;
...
C++ for Scientific Computing
141/316
Advanced Datatypes
Record Application: Sparse Matrices
As an example, consider the matrix
row
1
3
0
2
1
1
4 1 1
2
1
3
1
0
sparsematrix_t
entry_t *
NULL
NULL
1
0
NULL
1
3
3
2
NULL
S;
entry;
S.nrows = 4; S.ncolumns = 4;
S.entries = new entry_t[4];
// first row
entry = & S.entry[0];
entry->column = 0; entry->coeff = 1.0; entry->next = new entry_t;
entry = entry->next;
entry->column = 2; entry->coeff = 3.0; entry->next = NULL;
...
C++ for Scientific Computing
141/316
Advanced Datatypes
Record Application: Sparse Matrices
Matrix-Vector multiplication:
void
mul_vec ( const real_t
alpha, const sparsematrix_t & S,
const vector_t x, vector_t & y )
{
for ( unsigned i = 0; i < S.nrows; i++ )
{
real_t
f
= 0.0;
entry_t * entry = & S.entries[i];
while ( entry != NULL )
{
f
+= entry->coeff * x[ entry->column ];
entry = entry->next;
}
y[ i ] += alpha * f;
}
}
142/316
Advanced Datatypes
Record Application: Sparse Matrices (Version 2)
We can store sparse matrices even more memory efficient, without
pointers. For this, well use three arrays:
colind: stores column indices for all entries, sorted by row,
coeffs: stores all coefficients in same order as in colind and
rowptr: stores at rowind[i] the position of the first values
143/316
Advanced Datatypes
Record Application: Sparse Matrices (Version 2)
For the matrix
2
1
4 1 1
1
3
the corresponding source code is:
crsmatrix_t
unsigned
unsigned
real_t
S;
rowptr[] = { 0, 2, 4, 7, 9 };
colind[] = { 0, 2,
1, 3,
0, 1, 2,
coeffs[] = { 1, 3,
2, -1,
-4, -1, 1,
0, 3 };
1, 3 };
S.nrows = 4; S.ncolumns = 4;
S.rowptr = rowptr;
S.colind = colind;
S.coeffs = coeffs;
144/316
Advanced Datatypes
Record Application: Sparse Matrices (Version 2)
Matrix-Vector multiplication:
void
mul_vec ( const real_t
alpha, const crsmatrix_t & S,
const vector_t x, vector_t & y )
{
for ( unsigned row = 0; row < S.nrows; row++ )
{
real_t
f = 0.0;
const unsigned lb = S.rowptr[ row ];
const unsigned ub = S.rowptr[ row+1 ];
for ( unsigned j = lb; j < ub; j++ )
f += S.coeffs[ j ] * x[ S.colind[ j ] ];
y[ i ] += alpha * f;
}
}
145/316
Advanced Datatypes
Enumerations
A special datatype is available to define enumerations:
enum henum namei {
hname 1i, hname 2i, . . ., hname ni
};
Example:
enum matrix_type_t { unsymmetric, symmetric, hermitian };
matrix_type_t
t;
i f ( t == symmetric ) { ... }
146/316
Advanced Datatypes
Enumerations
One can also define the value of the enumeration members
explicitly:
enum matrix_size_t { small = 10, middle = 1000, large = 1000000 };
if
( nrows < small ) { ... }
else i f ( nrows < middle ) { ... }
else i f ( nrows < large ) { ... }
else
{ ... }
...; break;
...; break;
case unsymmetric:
default:
...;
}
C++ for Scientific Computing
147/316
Advanced Datatypes
Unions
A union is a special record datatype where all variables share the
same memory, i.e. changing one variable changes all other
variables.
union hunion namei {
hdatatype 1i hname 1i;
..
.
hdatatype ni hname ni;
};
Example:
union utype_t {
int
n1;
int
n2;
float f;
};
utype_t
u;
u.n1 = 2;
cout << u.n2 << endl;
cout << u.f << endl;
// yields 2
// ???
148/316
Advanced Datatypes
Unions
Unions can also be used inside records:
enum smat_type_t { ptr_based, crs_based };
struct general_sparse_matrix_t {
smat_type_t type;
union {
sparsematrix_t
crsmatrix_t
} matrix;
ptrmat;
crsmat;
};
general_sparse_matrix_t
S;
S.type = ptr_based;
S.matrix.ptrmat.nrows = 10;
Remark
Typical usage for unions: save memory for different
representations.
C++ for Scientific Computing
149/316
Advanced Datatypes
Unions
The name matrix of the union can be omitted. The access is
then as if it were a direct member of the struct.
enum smat_type_t { ptr_based, crs_based };
struct general_sparse_matrix_t {
smat_type_t type;
union {
sparsematrix_t
crsmatrix_t
};
ptrmat;
crsmat;
};
general_sparse_matrix_t
S;
S.type = ptr_based;
S.ptrmat.nrows = 10;
150/316
151/316
152/316
int main ()
{
f( 42, 3.1415926 );
}
153/316
154/316
155/316
156/316
157/316
JOBU, JOBVT
INFO, LDA, LDU, LDVT, LWORK, M, N
A( LDA, * ), S( * ), U( LDU, * ),
VT( LDVT, * ), WORK( * )
char *,
int *
and
double *.
158/316
159/316
*
*
*
n
M
jobu
jobv
info
lwork
work
S
VT
=
=
=
=
=
=
=
=
=
10;
new double[
O;
S;
0;
10*n*n;
new double[
new double[
new double[
n*n ];
// overwrite M with U
// store V^T in VT
work ]; // workspace for dgesvd
n ];
n*n ];
... // fill M
dgesvd_( & jobu, & jobv, & n, & n, M, & n, S, M, & n, V, & ldv,
work, & lwork, & info );
160/316
int main ()
{
f( 42, 3.1415926 );
}
int main ()
{
f( 42, 3.1415926 );
}
// FILE: file2.hh
#include "file2.hh"
#include "file1.hh"
...
...
161/316
162/316
// FILE: file2.hh
#ifndef __FILE1_HH
#define __FILE1_HH
#ifndef __FILE2_HH
#define __FILE2_HH
#include "file2.hh"
#include "file1.hh"
#endif
#endif
163/316
164/316
165/316
__REAL_HH
__REAL_HH
typedef double
const real_t
extern real_t
extern int
real_t;
PI = 3.1415926;
eps;
stepwidth;
#endif
166/316
// for real_t
eps
= 1e-8;
stepwidth = 1024;
167/316
// source
static real_t eps = 1e-8;
aeps );
void
set_eps ( const real_t aeps )
{
if
( aeps < 0.0 ) eps = 0.0;
else i f ( aeps > 1.0 ) eps = 1.0;
else
eps = aeps;
}
168/316
169/316
init
( ... );
mul_vec ( ... );
and you want to join that with another module for sparse matrices:
struct matrix_t {
size_t
nrows, ncolumns, nnzero;
size_t * rowptr, * colind;
real_t * coeffs;
};
matrix_t * init
( ... );
void
mul_vec ( ... );
170/316
171/316
namespace Sparse {
struct matrix_t {
unsigned nrows, ncolumns;
real_t * coeffs;
};
struct matrix_t {
unsigned
nrows, ncolumns, nnzero;
unsigned * rowptr, * colind;
real_t *
coeffs;
};
matrix_t *
void
matrix_t * init
( ... );
void
mul_vec ( ... );
init
( ... );
mul_vec ( ... );
172/316
D = Dense::init( 10, 10 );
S = Sparse::init( 10, 10, 28 );
Dense::mul_vec( 1.0, D, x, y );
Sparse::mul_vec( 1.0, S, x, y );
173/316
D = init( 10, 10 );
// call to Dense::init
S = init( 10, 10, 28 ); // call to Sparse::init
mul_vec( 1.0, D, x, y );
mul_vec( 1.0, S, x, y );
// call to Dense::mul_vec
// call to Sparse::mul_vec
Remark
Remember, that types must have different names.
Hence, the types for D and S have to be named with their
namespaces.
174/316
#include "dense.hh"
#include "vector.hh"
#include "sparse.hh"
// vector definitions
void
f ( matrix_t & M );
175/316
D = De::init( 10, 10 );
S = Sp::init( 10, 10, 28 );
De::mul_vec( 1.0, D, x, y );
Sp::mul_vec( 1.0, S, x, y );
176/316
namespace LinAlg {
namespace Sparse {
...
}
}
177/316
178/316
// module 2
namespace {
void f () { ... }
}
void h ()
{
f(); // Error: unknown
// function "f"
}
void g ()
{
f(); // Ok: same module
}
Such functions are therefore hidden for other modules and purely
local to the corresponding module.
179/316
180/316
PI
= 3.14159265358979323846;
sqrtPI = sqrt( PI );
is equivalent to
const real_t
const real_t
PI
= 3.14159265358979323846;
sqrtPI = std::sqrt( PI );
181/316
Classes
182/316
Classes
Records with Functions
Records were previously introduced only as a way to group data. In
C++, records can also be associated with functions.
struct vector_t {
unsigned size;
real_t * coeffs;
void init ( const unsigned
void fill ( const real_t
void scale ( const real_t
n );
f );
f );
};
183/316
Classes
Records with Functions
A record function is called specifically for an instance of a record,
using the same dot operator . as for record variables:
int main ()
{
vector_t
x;
x.init( 10 );
x.fill( 1.0 );
x.scale( 5.0 );
return 0;
}
184/316
Classes
Record Function Implementation
Inside a record function, one has implicit access to all record
variables of the specific record object, for which the function was
called. Therefore, the following two functions are equivalent:
void
vector_t::init ( const uint n )
{
size
= n;
coeffs = new real_t[n];
}
void
init ( vector_t * x,
const uint n )
{
x->size
= n;
x->coeffs = new real_t[n];
}
...
...
x.init( 10 );
init( x, 10 );
185/316
Classes
Record Function Implementation
A pointer to the corresponding record object is actually available in
C++. It is called this. Hence, one can also write:
void vector_t::init ( const uint n )
{
this ->size
= n;
this ->coeffs = new real_t[n];
}
n )
// inline function
186/316
Classes
Records: const functions
Member functions not changing the record data should be defined
as const functions:
struct vector_t {
...
void
scale ( const real_t
real_t norm2 () const;
...
};
f );
x( 10 );
187/316
Classes
Records: Constructors and Destructors
There are special functions for each record:
constructors: functions automatically called when a record type is
instantiated, e.g. by new, and
destructor: a function automatically called when a record
variable is destroyed, e.g. by delete.
The name of a constructor function is identical to the name of the
record, whereas the name of the destructor is the record name
prefixed by :
struct vector_t {
unsigned size;
real_t * coeffs;
vector_t ();
// constructor 1
vector_t ( const unsigned n ); // constructor 2
~vector_t ();
// destructor
};
188/316
Classes
Records: Constructors and Destructors
By definition, constructors should create necessary resources for the
object, whereas destructors should free all record object resources:
vector_t::vector_t ()
{
size
= 0;
coeffs = NULL;
}
vector_t::vector_t ( const uint
{
size
= n;
coeffs = new real_t[n];
}
vector_t::~vector_t ()
{
delete[] coeffs;
}
n )
Remark
Constructors and the destructor have no return type.
Furthermore, destructors must not have function
arguments.
C++ for Scientific Computing
189/316
Classes
Records: Constructors and Destructors
Example 1: instantiated and destroyed explicitly by new and
delete:
vector_t * x = new vector_t();
vector_t * y = new vector_t( 10 );
// calling constructor 1
// calling constructor 2
y->fill( 1.0 );
delete x;
// calling destructor
Remark
If the constructor has no arguments, the corresponding
parentheses can be omitted:
vector_t * x = new vector_t;
// calling constructor 1
190/316
Classes
Records: Constructors and Destructors
Example 2: instantiated and destroyed implicitly by block scope:
{
vector_t
vector_t
x;
y( 10 );
// calling constructor 1
// calling constructor 2
y.fill( 1.0 );
}
Here, the record objects are not pointers. When leaving the block,
the destructors of all record objects are called automatically!
Thereby, it is ensured, that all resources are released.
191/316
Classes
Records: Special Constructors
By default, each record type has two constructors already
implemented by the C++ compiler: the default constructor and the
copy constructor.
The default constructor is a constructor without arguments, e.g.
constructor 1 in the previous example. The copy constructor is a
constructor with one argument being a constant reference to an
object of the same type as the record itself:
struct vector_t {
...
vector_t ( const vector_t & x );
...
};
// copy constructor
x( 10 );
y( x );
z = y;
Classes
Records: Special Constructors
The default constructor implemented by the C++ compiler does
nothing, e.g. it does not initialise the member variables. Hence,
without a user implemented default constructor, the values of the
member variables are random (ref. Coding Principle No. 3).
The C++ generated copy constructor simply copies the data in
each member variable of the record:
vector_t *
vector_t *
x = new vector_t( 10 );
y = new vector_t( & x ); // now: x.coeffs == y.coeffs,
// e.g. equal pointers
193/316
Classes
Records: Special Constructors
For vectors, instead of the pointers, the content of the array should
be copied:
vector_t::vector_t ( const vector_t & v )
{
size
= v.size;
coeffs = new real_t[ size ];
for ( uint i = 0; i < size; i++ ) coeffs[i] = v.coeffs[i];
}
x = new vector_t( 10 );
y = new vector_t( & x );
x->coeffs[ 5 ] = 2;
y->coeffs[ 3 ] = 4;
194/316
Classes
Records: Special Constructors
To sum this up:
Coding Principle No. 22
Always make sure, that the C++ generated default and
copy constructors behave as expected. If in doubt:
implement constructors by yourself.
195/316
Classes
Records: Visibility
All member variables and functions of a record were visible and
accessible from any other function or datatype in C++. This makes
illegal changes to the data in a record possible, e.g. change the
size variable of vector t without also changing the coeffs
variable.
To prevent this behaviour, one can change the visibility of variables
and functions using one of the following keywords:
public: variables or functions can be accessed without
restrictions,
protected: variables or functions can only be accessed by member
functions of the record type or by derived records (see
later),
private: variables or functions can only be accessed by member
functions of the record type.
C++ for Scientific Computing
196/316
Classes
Records: Visibility
Example 1:
struct vector_t {
private:
size_t
size;
real_t * coeffs;
public:
...
size_t
};
...
{
vector_t
x( 10 );
197/316
Classes
Records: Visibility
Example 2:
struct vector_t {
private:
size_t
size;
real_t * coeffs;
public:
...
protected:
void init ( const size_t n )
{
size
= n;
// Ok: <size> is visible to member functions
coeffs = new real_t[n];
}
};
{
vector_t
x( 10 );
x.init( 20 );
}
C++ for Scientific Computing
198/316
Classes
Records: Visibility
Illegal states of member variables can be prevented by making
them private and allowing modifications only via public member
functions:
struct vector_t {
private:
size_t
size;
real_t * coeffs;
public:
...
size_t
void
...
};
get_size () const
set_size ( const size_t
{ return size; }
n ) { init( n ); }
199/316
Classes
Records: Operator Overloading
In C++, operators, e.g. +, *, = or [], can be defined for record
datatypes to simplify access or to ensure correct behaviour.
Depending on the operator, it can be defined inside or outside the
record definition, e.g. operators changing the object like = or +=
in the record definition and binary operators working on two
objects typically outside the record.
In terms of syntax, operator functions are treated like any other
function. The name of the operator is defined by
operator hoperator namei
e.g.
operator
operator
operator
operator
C++ for Scientific Computing
=
+
*
[]
200/316
Classes
Records: Operator Overloading (Example)
struct vector_t {
...
// provide index operator for vectors
real_t operator [] ( const size_t i ) { return coeffs[i]; }
// arithmetics
vector_t & operator
{
for ( size_t i =
return *this;
}
vector_t & operator
vector_t & operator
{
for ( size_t i =
return *this;
}
...
};
{ ...
x += y;
y *= 2.0;
}
C++ for Scientific Computing
201/316
Classes
Records: Operator Overloading
Be very careful when overloading the standard arithmetic
operators, e.g. + or *, since that can lead to very inefficient code:
// vector addition
vector_t operator + ( const vector_t &
{
vector_t t( v1 );
return ( t += v2 );
}
v2 )
202/316
Classes
Records: Special Operators
The analog to the copy constructor is the copy operator =, e.g.
used in
x = y;
203/316
Classes
Records: Special Operators
Copy operator for vectors:
struct vector_t {
...
vector_t & operator = ( const vector_t &
{
init( v.size );
v )
204/316
Classes
Records: Special Operators
The copy operator also allows a simplified copy constructor:
vector_t::vector_t ( const vector_t & v )
{
*this = v;
}
205/316
Classes
Classes
Records provide all mechanisms for object-oriented programming.
What about classes?
C++ also provides a class type, e.g.
class hclass namei {
..
.
};
Classes in C++ are identical to records, except for one little
difference: if the visibility specifiers, e.g. public, protected and
private, are missing, by default all member variables and functions
are private in a class and public in a record.
Therefore, it is up to you, which form you prefer: classes or records.
One possible rule: for simple records without functions use a
struct, otherwise use a class. But remember Coding Principle
No. 22, Coding Principle No. 25 and Coding Principle No. 3 (RAII).
C++ for Scientific Computing
206/316
Classes
Application: BLAS (Version 3)
Vector type:
class vector_t {
private:
size_t
size;
real_t * coeffs;
public:
vector_t ( const size_t
vector_t ( const vector_t &
~vector_t ();
size_t
n = 0 );
x );
get_size () const;
real_t
operator [] ( const size_t
real_t & operator [] ( const size_t
void fill ( const real_t
void scale ( const real_t
void add
( const real_t
i ) const;
i );
f );
f );
f, const vector_t & x );
207/316
Classes
Application: BLAS (Version 3)
Remarks to the vector class:
The constructor
vector_t ( const size_t
n = 0 );
i )
208/316
Classes
Application: BLAS (Version 3)
Matrix type:
class matrix_t {
private:
size_t
nrows, ncolumns;
real_t * coeffs;
public:
matrix_t ( const size_t n, const size_t
matrix_t ( const matrix_t & M );
~matrix_t ();
size_t
size_t
m );
get_nrows
() const;
get_ncolumns () const;
real_t
operator [] ( const size_t i, const size_t j ) const;
real_t & operator [] ( const size_t i, const size_t j );
void fill ( const real_t
void scale ( const real_t
void add
( const real_t
f );
f );
f, const matrix_t & M );
209/316
Classes
Application: BLAS (Version 3)
void mul_vec ( const real_t
vector_t &
Remarks:
The private default constructor prevents accidental matrices
210/316
Classes
Application: BLAS (Version 3)
matrix_t * M = new matrix_t( 10, 10 );
vector_t * x = new vector_t( 10 );
vector_t * y = new vector_t( 10 );
x.fill( 1.0 );
y.fill( 0.0 );
...
M[3,4] = ... // fill matrix M
...
cout << M.normF() << endl;
M.mul_vec( -1.0, x, y );
delete x;
delete y;
delete M;
211/316
Generic Programming
212/316
Generic Programming
Generic Programming
Consider
int
float
double
213/316
Generic Programming
Templates
C++ supports generic programming in the form of templates.
A template function is defined as
template < typename hnamei >
hreturn typei hfunction namei ( hargument listi )
hfunction bodyi
e.g.
template <typename T>
T square ( const T f ) { return f*f; }
214/316
Generic Programming
Templates
Similar defined are template record (or class) types:
template < typename hnamei >
struct hrecord namei {
..
.
}
Example for a simple list:
template <typename T>
struct list {
T
element;
list * next;
};
215/316
Generic Programming
Templates
Template datatypes are used by explicitly defining the
corresponding type to be used in the template. The syntax is:
hrecord namei< htypei >
For the list type, this looks as:
int main ()
{
list< int >
list< double > *
ilist.element
ilist.next
ilist;
dlist = new list< double >;
= 2;
= NULL;
dlist->element = 2.0;
dlist->next
= NULL;
}
216/316
Generic Programming
Templates
The template type can be a template by itself:
int main ()
{
list< list< float > >
fllist;
fllist.element.element = 2.0f;
fllist.element.next
= NULL;
fllist.next
= NULL;
}
217/316
Generic Programming
Templates
Template functions can also be explicitly typed using the same
syntax:
float sq2 = square< float >( 2.0f );
double sq3 = square< double >( 3.0 );
n1
n2
n3
n4
=
=
=
=
10;
20;
min( n1, n2 );
// ambiguous: int or unsigned?
min<int>( n1, n2 ); // ok: int explictly chosen
218/316
Generic Programming
Templates
Since template functions or types are generated at compile time,
the full specification, e.g. function header and body, has to be
available whenever such a function is used. Therefore, template
functions must always be implemented in a header file.
Remark
If many template types and functions are used for many
different datatypes, this significantly can bloat the
resulting compiled program. Also, the compilation time
can be much higher.
219/316
Generic Programming
Templates: Example (CG iteration for Ax = b)
template <typename T_MAT> void
cg ( const T_MAT & A, vector_t & x, const vector_t & b )
{
vector_t r( b ), p, a( x.get_size() );
mul_vec( -1.0, A, x, r );
p = r;
// r = b - A x
// p = r
220/316
Generic Programming
Templates: Example (CG iteration for Ax = b)
Remark
Works for arbitrary matrix types, e.g. dense and sparse,
as long as mul vec is implemented for each type:
Dense::matrix_t
vector_t
vector_t
M( n, n );
x( n );
b( n );
cg( M, x, b );
Sparse::matrix_t
S( n, n, nnonzero );
cg( S, x, b );
221/316
Generic Programming
Templates: Multiple Template Arguments
Template functions and datatypes are also possible with more than
one argument type:
template <typename T_MAT, typename T_VEC> void
cg ( const T_MAT & A, T_VEC & x, const T_VEC & b )
{
...
}
222/316
Generic Programming
Templates: Value Templates
Previous templates were used to leave out the type to work on.
One can also predefine the type and leave out the specific value:
template <int N>
struct smallvector_t {
real_t coeffs[N];
smallvector_t ()
{
for ( size_t i = 0; i < N; i++ )
coeffs[i] = 0.0;
}
smallvector_t & operator += ( const smallvector_t & v ) { ... }
smallvector_t & operator -= ( const smallvector_t & v ) { ... }
smallvector_t & operator *= ( const real_t
f ) { ... }
};
template <int N>
smallvector<N> operator + ( const smallvector_t<N> & v1,
const smallvector_t<N> & v2 ) { ... }
223/316
Generic Programming
Templates: Value Templates
smallvector_t<3>
x, y, z;
x += y;
y *= 2.0;
z = x -= z + y;
Remark
As long as N is small, e.g. 5, the arithmetic operators
are still very efficient.
224/316
Generic Programming
Templates: Conclusion
Templates are a very powerful tool to simplify programming.
Furthermore, they are Turing complete, i.e. you can program every
computable program by only using templates.
Unfortunately, templates are also quite complex and involve a
sginificant overhead in terms of programming, compilation time
and (possibly) program size. Therefore, it is suggested to use them
sparsely.
225/316
Error Handling
226/316
Error Handling
General Thoughts
Consider
vector_t
vector_t
x( 10 );
y( 20 );
y = x;
227/316
Error Handling
General Thoughts
Another typical error involves NULL pointers:
bool is_in ( const node_t & root, const int val )
{
i f ( root.val == val ) return true;
else
return is_in( * root.son1, val ) ||
is_in( * root.son2, val );
}
Here, the recursive call to is in for the sons is only possible, if the
sons are not NULL, which is not tested.
228/316
Error Handling
General Thoughts
The following example for a LU factorisation of a dense matrix
shows another problem:
for ( uint j = 0; j <= min(nrows,ncolumns); ++j )
{
i f ( j < n-1 )
scale( nrows-1-j, 1.0 / A[j, j], & A[j + 1, j] );
i f ( j < mrc )
// rank-1 update
geru( nrows-1-j, ncolumns-1-j, -1.0,
& A[j + 1, j], 1,
& A[j, j + 1], n,
& A[j + 1, j + 1], n );
}
229/316
Error Handling
General Thoughts
Obviously, we need some protection against possible errors. Even
more important is, that errors are detected and reported.
For detecting errors:
Coding Principle No. 26
Always check function arguments, especially pointers
for illegal values (pre condition of a function).
If a critical situation is possible by an instruction,
check the operands of the instruction.
Check, whether the results of a function are as
expected (function post condition).
230/316
Error Handling
General Thoughts
For reporting errors different strategies are possible:
by message: Detect the error and print a (detailed) message that
a problem occured, but otherwise continue the
program. This is usuable for non-critical errors.
by abort: Detect the error and abort the program, possibly
with a detailed description of the error. This
behaviour is acceptable for short running programs.
by exception: Detect the error and throw an exception and let the
programmer either catch the exception or not, in
which case the program is aborted.
231/316
Error Handling
Assertions
Assertions fall into the category error handling by abort. An
assertions is defined using the function assert defined in the
module cassert.
Remark
Assertions are also possible in C, therefore the filename
cassert.
The assert function expects a logical expression as its function
argument. If the expression is false, the program is terminated,
printing the definition of the failed assertion together with the file
name and line number, where the assertion failed.
232/316
Error Handling
Assertions
For the copy operator of vector t this looks as:
#include <cassert>
struct vector_t {
...
vector_t & operator = ( const vector_t & x )
{
assert( get_size() == x.get_size() );
for ( size_t i = 0; i < size; i++ )
(*this)[i] = x[i];
}
}
Now, if x has a different size than the local vector, the application
will immediately abort.
233/316
Error Handling
Assertions and Optimisation
When using optimisation to translate the source code into machine
code, assertions are usually omitted. This is an advantage and a
disadvantage:
one can put quite expensive tests into assertions, which are
Therefore, it should be used only if one can test all cases without
optimisation and simply want to have a fast program for such
known and tested cases.
234/316
Error Handling
Assertion Usage
The size of the logical expression in the assertion is not limited,
but large expressions do not give the programmer the information,
he seeks:
void
mul_mat ( const real_t alpha, const matrix_t & A, const matrix_t & B,
matrix_t & C )
{
assert( A.get_nrows()
== C.get_nrows()
&&
B.get_ncolumns() == C.get_ncolumns() &&
A.get_ncolumns() == B.get_nrows() );
...
}
235/316
Error Handling
Assertion Usage
Alternatively the assertion can be split, resulting in more
information if some incompatible matrices are used:
void
mul_mat ( const real_t alpha, const matrix_t & A, const matrix_t & B,
matrix_t & C )
{
assert( A.get_nrows()
== C.get_nrows()
);
assert( B.get_ncolumns() == C.get_ncolumns() );
assert( A.get_ncolumns() == B.get_nrows()
);
...
}
236/316
Error Handling
Assertion Conclusion
Assertions are suitable for
non-critical programs or programs with a short runtime,
for errors, which can be detected in a previous test mode of
the program.
Assertions are not suitable for
programs which shall not terminate or have a very long
runtime,
programs with many different input data.
237/316
Error Handling
By Return Value
In C, the error status of a function was typically reported by the
return value, e.g. if non-zero, an error occured (see main). This is
not encouraged:
Since the return code of every function has to be tested, the
...
...
...
...
...
...
}
}
}
}
}
}
//
//
//
//
//
//
Error
Ok
Error
Ok
Error
Ok
238/316
Error Handling
Exceptions
C++ allows to try to execute portions of the source code and
catch detected errors by special handlers. Furthermore, the error
information is supplied in the form of an arbitrary datatype,
thereby allowing to provide further information about the error.
This mechanism is called exception handling and the error objects
(or the errors) are exceptions.
The syntax for exception handling is:
try
hblocki
catch ( herror typei herrori )
hblocki
Here, herror typei can be any datatype and herrori is the
corresponding error variable.
C++ for Scientific Computing
239/316
Error Handling
Throwing Exceptions
Example:
vector_t
vector_t
x( 10 );
y( 20 );
try
{
y = x;
}
catch ( VectorSizeErr &
{
... // handle error
}
e )
240/316
Error Handling
Exception Types
The type of the exception can be defined according to the needs of
the programmer, no limitations are set. Example with a class:
class VectorSizeErr
{
public:
const char * what () const
{
return "vector sizes differ";
}
};
241/316
Error Handling
Exception Types
Alternatively, the error can be more described:
class VectorSizeErr
{
const char * err_function;
public:
VectorSizeErr ( const char * err_fn )
{
err_function = err_fn;
}
const char *
const char *
};
242/316
Error Handling
Exception Types
Now, the function where the error occured is stored while throwing
the exception:
vector_t & operator = ( const vector_t & x )
{
i f ( get_size() == x.get_size() )
throw VectorSizeErr( "vector::operator =" );
...
}
243/316
Error Handling
Multiple Exception Handlers
C++ permits to implement multiple exception handlers for a single
try block:
try
{
y
= x;
x[30] = 1.0;
// throws ArrayBoundErr
}
catch ( VectorSizeErr & e )
{
...
}
catch ( ArrayBoundErr & e )
{
...
}
244/316
Error Handling
General Exception Handlers
To catch all exceptions with a single handler, the following
construct is allowed:
try
{
y
= x;
x[30] = 1.0;
}
catch ( ... )
{
cout << "exception occured" << endl;
}
245/316
Error Handling
Exceptions and Functions
For a C++ function one can define the exception types the function
and all directly or indirectly called functions can throw by using
hfunction headeri throw( hexception listi );
e.g.:
void mul_mat ( ... ) throw( MatrixSizeErr, ArrayBoundErr );
246/316
Error Handling
Predefined Exceptions
Some exceptions are already defined by the C++ library, e.g.
bad alloc: thrown by new if no memory is left to serve the
allocation request
bad exception: thrown, if no exception handler is found for a
previously thrown exception.
These exceptions are defined in the module exception of the std
namespace.
247/316
Error Handling
Rethrow an Exception
A catched exception can be thrown again by the exception handler,
thereby forwarding the handling of the error to another exception
handler:
try
{
...
}
catch ( ArrayBoundErr & e )
{
...
throw; // rethrow exception
}
Remark
The instruction throw; is only allowed in an exception
handler.
C++ for Scientific Computing
248/316
Error Handling
Stack Unwinding
Suppose that an error occures inside a complicated recursive
function, which allocates many variables, e.g.
void f ( ... )
{
record1_t r1;
...
f( ... );
...
record2_t r2;
...
f( ... );
...
}
249/316
Error Handling
Stack Unwinding
Remark
Unfortunately, this does not hold for objects allocated by
new. But that can be fixed (see later).
250/316
Error Handling
Exceptions: Conclusion
Exceptions a superior to all other form of error handling, e.g.
they do not interfere with the normal algorithm like the usage
of return values,
the program does not need to terminate as with assertions,
specific handling can be implemented for specific errors,
the cleanup of record objects is provided,
etc.
Hence:
Coding Principle No. 27
Use exceptions for all error handling.
In the simplest case, implement the throw instructions for all
errors and wrap your main function in a try-catch construct.
C++ for Scientific Computing
251/316
252/316
Remark
Unfortunately, although the individual functions and
types are defined by the C++ standard, not all compilers
support all features.
C++ for Scientific Computing
253/316
254/316
255/316
Remark
Remember to split declaration and implementation into
header and source file or to use the inline keyword.
256/316
n;
cin >> n;
n1, n2;
f1;
f2;
The order in which the elements are read from the stream is
defined by the order at which the appear in the statement, e.g. n1
first and f2 last.
C++ for Scientific Computing
257/316
258/316
x;
file( "output" );
259/316
x;
file( "input" )
file >> x;
260/316
file( "input" );
i f ( ! file.is_open() )
throw "could not open file";
261/316
262/316
263/316
264/316
cont;
iter;
first = cont.begin();
last = cont.end();
265/316
iter = cont.begin();
266/316
e.g.:
#include <list>
...
std::list< int >
std::list< double * >
std::list< std::list< double > >
list1;
list2;
list3;
267/316
268/316
ilist;
ilist.push_front(
ilist.push_front(
ilist.push_back(
ilist.push_back(
1
2
3
4
);
);
);
);
for ( list<int>::iterator
it != ilist.end();
++it )
cout << *it << endl;
int
it = ilist.begin();
sum = 0;
while ( ! ilist.empty() )
{
sum += ilist.front();
ilist.pop_front();
}
269/316
vector1;
vector2;
vector3;
270/316
271/316
Examples:
#include <valarray>
...
std::valarray< int >
std::valarray< double >
array1( 10 );
array1( 1000 );
272/316
273/316
coeffs;
public:
vector_t ( const size_t
vector_t ( const vector_t &
n ) { coeffs.resize( n ); }
v );
v )
};
274/316
275/316
str1;
// empty string
str2( "Hello World" ); // preinitialised string
276/316
str1( "Hello" );
str2( "World" );
str3;
277/316
278/316
c1;
c2;
Complex objects store the real and the imaginary part in two
public member variables: re and im.
c1.re = 1.0;
c1.im = -2.0;
279/316
double
double
double
double
>
>
>
>
I( 0.0, 1.0 );
r( 5.0 );
z;
i = I;
// == 5 + 0i
// == 0 + 0i
c1 = c2 * c3;
c2 = 2.0 + c3;
c3 = std::sqrt( -3.0 * c1 + c2 / c3 );
280/316
The STL provides a solution for this problem: the class auto ptr.
An auto pointer is a class with a single data element, a pointer of
the specified type:
template <typename T>
class auto_ptr
{
private:
T * ptr;
...
};
C++ for Scientific Computing
281/316
x( new vector_t( 10 ) );
x->fill( 2.0 );
cout << x->norm2() << endl;
One can even access the pointer stored in the auto pointer:
vector_t * y = x.get();
282/316
x( new vector_t( 10 ) );
x( 10 );
y( new vector_t( 10 ) );
283/316
v( new vector_t( 10 ) );
...
return v.release(); // a previous exception would delete v
}
284/316
Class Inheritance
285/316
Class Inheritance
Inheritance
In an earlier section, generic programming was used to provide a
single implementation of an algorithm for multiple types. There is
another way to do that in C++: by using object oriented
programming or OOP.
In OOP one defines a hierarchy of classes, connected via
inheritance, sharing a set of functions, which then can be called for
all members of the hierarchy.
As an example, we will consider matrices. Up to now we had:
matrix t: a dense matrix,
sparsematrix t: a sparse matrix using lists and pointers and
crsmatrix t: a sparse matrix using arrays.
286/316
Class Inheritance
Inheritance
All of these matrix types had similar functions:
size_t
size_t
get_nrows
() const;
get_ncolumns () const;
f );
f );
f, const matrix_t & x );
287/316
Class Inheritance
Base Classes
Now, let us define a general matrix type, with only the set of
common functions and data to all other matrix types:
class matrix_t {
private:
size_t nrows, ncolumns;
public:
matrix_t ( const size_t n, const size_t
matrix_t ( const matrix_t & M );
size_t
size_t
void
void
void
void
m );
get_nrows
() const { return nrows };
get_ncolumns () const { return ncolumns };
fill
scale
add
mul_vec
real_t normF
const real_t
const real_t
const real_t
const real_t
vector_t &
() const;
(
(
(
(
f );
f );
f, const matrix_t & x );
alpha, const vector_t & x,
y ) const;
};
288/316
Class Inheritance
Base Classes
Although the base class defines all functions that can be used with
a general matrix, it can not provide the corresponding
implementation since no data is provided. Therefore, most
functions, which are called methods in the terminology of OOP,
are actually empty, i.e. have no body.
In C++ this can be define by using
hfunction headeri = 0;
e.g.
void
f ) = 0;
289/316
Class Inheritance
Base Classes
The base class for matrices now looks like:
class matrix_t {
private:
size_t nrows, ncolumns;
public:
matrix_t ( const size_t n, const size_t
matrix_t ( const matrix_t & M );
size_t
size_t
void
void
void
void
m );
get_nrows
() const { return nrows };
get_ncolumns () const { return ncolumns };
fill
scale
add
mul_vec
real_t normF
const real_t
const real_t
const real_t
const real_t
vector_t &
() const = 0;
(
(
(
(
f ) = 0;
f ) = 0;
f, const matrix_t & x ) = 0;
alpha, const vector_t & x,
y ) const = 0;
};
290/316
Class Inheritance
Derived Classes
To really work with matrices, functionality has to be provided.
Since that is specific to specific matrix formats, one needs to
define new classes for each special matrix type.
But, since all matrix types can also be considered to be general
matrix, they are matrices in the end, this should also be described
in C++. For that, the new classes are derived from the base class:
class densematrix_t : matrix_t {
...
};
class sparsematrix_t : matrix_t {
...
};
291/316
Class Inheritance
Derived Classes
A derived class inherits all methods and variables of the base class,
e.g. the variables nrows and ncolumns and the methods
get nrows and get ncolumns. Objects of the derived classes can
therefore use all methods which were already implemented in the
base class without providing an implementation:
densematrix_t
M;
cout << M.get_nrows() << " x " << M.get_ncolumns() << endl;
292/316
Class Inheritance
Derived Classes
The derived classes now need to provide functionality, e.g.
class densematrix_t : matrix_t {
private:
std::valarray< real_t >
coeffs;
public:
...
void
...
};
f );
f )
293/316
Class Inheritance
Derived Classes
The same can be done for sparse matrices:
class sparsematrix_t : matrix_t {
private:
std::valarray< size_t >
rowptr;
std::valarray< size_t >
colind;
std::valarray< real_t >
coeffs;
public:
...
void
...
};
f );
f )
294/316
Class Inheritance
Polymorphism
Now, if objects of each derived class are created:
densematrix_t
sparsematrix_t
M( 10, 10 );
S( 10, 10, 28 );
the overloaded methods can be called like for any other class
previously presented:
M.scale( -1.0 );
S.scale( 10.0 );
* A;
A = & M;
A = & S;
C++ for Scientific Computing
Class Inheritance
Polymorphism
But what happens when one calls
A = & M; A->scale( 1.5 );
A = & S; A->scale( 0.5 );
296/316
Class Inheritance
Polymorphism
This allows us to define a general algorithm, e.g. CG iteration, to
work only with the base class:
void cg ( const matrix_t & A, vector_t & x, const vector_t & b )
{
vector_t r( b ), p, a( x.get_size() );
A.mul_vec( -1.0, x, r );
p = r;
297/316
Class Inheritance
Polymorphism
But since all objects of derived classes are also objects of the base
class, the general algorithm can be used for these derived classes
too:
densematrix_t
vector_t
vector_t
D( n, n );
x( n );
b( n );
cg( D, x, b );
sparsematrix_t
S( n, n, nnonzero );
cg( S, x, b );
298/316
Class Inheritance
Virtual Methods
For polymorphism to actually work in C++, one thing has to be
added to the definition of methods in classes. Consider
class matrix_t {
...
void nullify () { scale( 0.0 ); }
}
Here, the method nullify in the base class matrix t makes use
of the method scale, which is only implemented in a derived
class. Such functions in a base class would not call the function in
the derived class:
densematrix_t
M.nullify();
M( 10, 10 );
// does not call densematrix_t::scale( 0.0 )
299/316
Class Inheritance
Virtual Methods
For polymorphism to work in such situations, these functions have
to be virtual:
class matrix_t {
private:
size_t nrows, ncolumns;
public:
matrix_t ( const size_t n, const size_t
matrix_t ( const matrix_t & M );
m );
size_t
size_t
get_nrows
() const { return nrows };
get_ncolumns () const { return ncolumns };
virtual
virtual
virtual
virtual
void
void
void
void
fill
scale
add
mul_vec
v i r t u a l real_t normF
const real_t
const real_t
const real_t
const real_t
vector_t &
() const = 0;
(
(
(
(
f ) = 0;
f ) = 0;
f, const matrix_t & x ) = 0;
alpha, const vector_t & x,
y ) const = 0;
};
300/316
Class Inheritance
Virtual Methods
The same holds for derived classes:
class densematrix_t : matrix_t {
...
v i r t u a l void scale ( const real_t
...
};
f );
f );
Remark
The keyword virtual is only neccessary in the method
declaration, not the implementation.
301/316
Class Inheritance
Virtual Methods
Now, the call to scale(0.0) in the function nullify of the base
class will be handled by the corresponding methods in the derived
classes:
densematrix_t
M.nullify();
M( 10, 10 );
// call to densematrix_t::scale( 0.0 )
Remark
If a class has a virtual function, it also must have a
virtual destructor. Constructors can not be virtual and
polymorphism does not work in constructors.
Remark
Abstract methods must also be virtual.
C++ for Scientific Computing
302/316
Class Inheritance
Extending the Hierarchy
In the beginning we had two different classes for a sparse matrix
but have implemented only the CRS version.
Instead of that, sparsematrix t should only serve as a base class
for sparse matrices. Hence, it should only implement common
functions and data:
class sparsematrix_t : matrix_t
{
private:
size_t nnonzero;
public:
sparsematrix_t ( const size_t
const size_t
size_t
n, const size_t
nnzero );
m,
Note that it does not overload any method from matrix t and is
therefore also an abstract class.
C++ for Scientific Computing
303/316
Class Inheritance
Extending the Hierarchy
The special sparse matrix types are then derived from the base
sparse matrix class:
class crssparsematrix_t : sparsematrix_t
{
private:
std::valarray< size_t >
rowptr, colind;
std::valarray< real_t >
coeffs;
public:
...
v i r t u a l void mul_vec ( ... ) const;
...
};
class ptrsparsematrix_t : sparsematrix_t
{
private:
std::valarray< entry_t >
entries;
public:
...
v i r t u a l void mul_vec ( ... ) const;
...
};
C++ for Scientific Computing
304/316
Class Inheritance
Extending the Hierarchy
The complete class hierarchy now is:
matrix t
densematrix t
sparsematrix t
crssparsematrix t
ptrsparsematrix t
S( 10, 10, 28 );
A = & S;
// call to crssparsematrix_t::scale
cg( S, x, b );
305/316
Class Inheritance
Visibility
Remember the visibily keywords public, protected and private for
variables and functions in a record or class, respectively. The last
only differ in the context of inheritance:
One can access protected variables or functions of base
class in a derived class but not private members.
class densematrix_t
{
...
void print ()
{
// Error: nrows and ncolumns are private in matrix_t
cout << nrows << " x " << ncolumns << endl;
}
}
306/316
Class Inheritance
Visibility
Visibility can also be defined during inheritance. The previous
examples always assumed:
class densematrix_t : public matrix_t
{
...
};
class sparsematrix_t : public matrix_t
{
...
};
class crssparsematrix_t : public sparsematrix_t
{
...
};
class ptrsparsematrix_t : public sparsematrix_t
{
...
};
C++ for Scientific Computing
307/316
Class Inheritance
Visibility
One can also define private inheritance:
class densematrix_t : private matrix_t
{
...
};
class sparsematrix_t : private matrix_t
{
...
};
The difference is, that all methods in the base class are now
private in the derived class and can not be called from outside.
308/316
Class Inheritance
Constructors
A constructor of a base class can also be called in the constructor
of a derived class:
class densematrix_t : public matrix_t
{
...
densematrix_t ( const size_t n, const size_t
: matrix_t( n, m )
{
coeffs.resize( n*m );
}
...
}
m )
309/316
Class Inheritance
Calling overloaded Functions
Methods of a base class can also be called in the derived class even
though they were overloaded:
class densematrix_t : public matrix_t
{
...
v i r t u a l void nullify ()
{
matrix_t::nullify(); // call nullify of base class
...
}
...
}
All methods in all base classes are available for that, not only the
direct ancestor.
310/316
Class Inheritance
Type Casting
We have discussed type casting operators, e.g. const cast,
static cast and dynamic cast. Especially the last one is
important for classes, as it uses runtime type information and
respects class hierarchy:
densematrix_t *
crssparsematrix_t *
ptrsparsematrix_t *
matrix_t *
= M;
311/316
Class Inheritance
Type Casting
A dynamic cast will only return the requested pointer value, i.e.
perform the cast, if the object to which the pointer directs is of
that type, e.g. explicitly of that type or implicitly due to
inheritance. Otherwise a NULL pointer is returned.
Remark
The complexity of a dynamic cast is not O (1), because
the class hierarchy has to be parsed up to the specific
datatype.
Coding Principle No. 28
Use dynamic casts for type casting of derived datatypes
as much as possible.
312/316
Appendix
313/316
Coding Principles
1 choice of floating point
type
12 function naming
2 variable names
3 RAII
14 array boundaries
4 const usage
16 deallocate pointer
17 string termination
7 parentheses
18 type naming
8 implicit casts
19 header encapsulation
20 global variables
21 anonymous namespaces
11 return in function
314/316
Coding Principles
23 data access in a record
24 operator overloading
25 default copy operator
26 detecting errors
27 use exceptions
28 dynamic casts
315/316
Literature
Bjarne Stroustrup,
The C++ Programming Language,
Addison-Wesley, 2000.
Scott Meyers,
Effective C++,
Addison-Wesley, 2005.
Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides,
Design Patterns,
Addison-Wesley, 1995.
The C++ Resources Network,
http://www.cplusplus.com/.
316/316