Programming Techniques for Scientific Simulations I
Authors: R. Barton based on work by T. Lausberg and R. Worreby
Contact: rbarton@ethz.ch, toml@ethz.ch, rworreby@ethz.ch
In relation to lecture by: Dr. Roger Käppeli
For corrections, questions, remarks please contact the current maintainer: rbarton@ethz.ch
Note: Offline cppreference is searchable via “Index” or “std Symbol Index” from homepage, use Crl+F
Fully revised chapters: 1-3, 9, 16-22
Recent Changes (see git also):
- Reorganisation: Added 5. C++ Features & Keywords, moved chapters around
- 6 Functors, Operators, small changes
- 14 Inheritance corrections, some inaccuracies previously
1. C++ compiling
1.1 Compile Process
Programs can be split into several files/libraries, compiled separately and finally linked together.
.ii
pre-processed files, with macros run ie#
lines.s
assembly files (already specific to a CPU/OS).o
are compiled files (object code).a
,.so
are compiled static, shared libraries respectively.out
or.exe
is the final program to be run.a.out
is the default name (machine code)
- Preprocessor, handles all lines starting with a
#
(Macros), outputs c++ text file - Compiler + Assembler, convert c++ code to assembly code
- Linker, combines compiled
.o
files to a runnable file
1.2 Compile via CLI
This process independent of compiler (c++, g++, clang) and options/flags can be added mostly in any order. You have to be careful with the order when linking multiple files
c++ [options] file1 file2...
c++ main.cpp timer.cpp -std=c++17 -o main.exe
Compile Options
-o
Set name of output (usually last flag)-Wall -Wextra -Wpedantic
Turn on more warnings (-Weverything
with clang)-std=c++11
Set C++ version-Dname=value
Define macro name (case-sensitive),=value
optional-
-lname
Link the library namedlibname.a
. If libx.a calls a function in liby.a, you need to link in the right order:-lx -ly
-Lpath
Set where to look for libraries. Use just-L.
to look in current dir-Ipath
(as in -i) Add directory to look for headers, often used for adding library headers -
-c
Compile files to.o
, don’t link-E
Preprocess only-S
Preprocess and compile to assembly files.s
-g
Generate additional debugging information--save-temps
Saves intermediate compile files: .ii .s .o--help
To see more options
Optimisation Options
-DNDEBUG
turn off debug code, such as asserts-O3
Set optimisation level 3 (possible 0-5, fast)-O
flags are shortcuts to turn on many different compiler optimisation techniques. Levels 4+ increase the risk of wrong code being generated.-Os
optimises a program for size instead of speed-Og:
enables optimisations that don’t affect debugging (gcc >= 4.8)-O0
(default level) switch off optimisations
-march=native
Minimum processor architecture to be run on, native is the architecture of the computer running the compiler on. Can also put other arch instead ofnative
.-fopt-info
gives you information on which optimisations have been done (gnu compilers)-funroll-loops
Will optimise short loops by writing out the iterations individually
Libraries
Static/shared libraries are essentially pre-compiled collections of .cpp
files with header files (.a
or .so
+ headers).
We include
the libraries’ header files, so we know how to interface with the library when compiling our own .o
files, i.e. knowing which functions exists, their input/outputs.
During the linking stage we combine the pre-compiled library and our .o
files, where the linker checks that the functions actually exists
Create static library:
c++ -c square.c
To compile files to .oar rcs libsquare.a square1.o square2.o
To create the library from compiled .o’s. include ‘lib’ as prefix to easily include later.r
means replace existing,c
don’t warn if lib doesn’t exist,s
create lib index
Create shared library: (a library that exists only once in memory, can be used by several programs at once)
c++ -fPIC -c square.c
To compile files to .o,-fPIC
for position independent codec++ -shared -fPIC -o libsquare.so square1.o square2.o
To create library from compiled .o’s.
Header Only Libraries
Libraries that are not pre-compiled and all code is in the headers, which means more compile time optimisations can be made (but is slower to compile)
Necessary for templated libraries, templates are always in the header as we need to create a compiled version per type that is used. E.g. Eigen
Example
square.hpp
double square(double x);
square.cpp
#include "square.hpp"
double square(double x) {
return x*x;
}
main.cpp
#include <iostream>
#include "square.hpp" //cpp not included
int main() {
std::cout << square(5.);
}
Compiling:
- Compile the file
square.cpp
, with the-c
option (no linking, createssquare.o
)
$ c++ -c square.cpp
- Compile the file
main.cpp
, linksquare.o
and outputsquare.exe
$ c++ main.cpp square.o -o square.exe
- Alternatively, compile
main.cpp
with no linking as in step 1. Then perform just the linking
$ c++ main.o square.o –o square.exe
Using Library:
$ c++ -c square.cpp
- Package square.o into a static library
$ ar rcs libsquare.a square.o
- Compile
main.cpp
and link the lib,-L.
to look for lib in current folder (signified by the.
)
$ c++ main.cpp -L. -lsquare
or directly
$ c++ main.cpp libsquare.a
After a while this gets tedious $\implies$ use a build system, e.g. make, cmake…
1.3 Pre-processor Macros
All lines starting with a #
are handled by the pre-processor (1st compilation step). These manipulate the cpp
text file, removing/adding/replacing text, before compilation.
Remember compiler flags: -DNAME=VALUE
to define a variable with an optional value and -E
to output only pre-processed .ii
file.
Common Macros:
#include <filename>
pastes the file into that line, brackets affect where to look for the file<>
looks in list of directories known to the compiler, where libs are installed, commonly/usr/include
“”
looks in the current directory first then where<>
looks, can specify a relative path e.g.#include “lib/simpson.h”
#define NAME VALUE
defines variable in code, value is optional (default is1
)- Important: then all occurrences of
NAME
below will be replaced byVALUE
- Using the
-D
compiler flag is equivalent to writing#define
on the first line #define NAME(param1, param2) function
advanced defines exist with parameters (see example)
- Important: then all occurrences of
#undef NAME
undefine/delete var#ifdef NAME
Checking if variables are defined (exist) is a good way of doing boolean logic, as the user only has to define the variables they are interested in when compiling.#if CONDITION
if statement, works with normal logical operators&&
,||
,!
etc- can also use
defined(NAME)
in the condition, to check ifNAME
is defined
- can also use
#endif
close an if-statement, like the}
in c++#else
,#elif
(else-if)#error message
throw error, printed to console (#warning
also exists)
Use Cases:
- Including files
- Conditional compilation, e.g. platform specific
- Control from terminal, how to compile
- Error checking, e.g. on invalid defined variables
- Text manipulation, replacement (not recommended)
Tips:
- Don’t use macros unless you have to, using this can lead to:
- Unexpected side effects
- Problems with debugger and other tools
- No type safety
- Use all caps names to prevent unwanted name conflicts with c++ code, causing the code to be replaced
Macros are very important in C but have far fewer uses in C++. Almost every macro demonstrates a flaw in the programming language or programmer. Because they rearrange the program text before the compiler properly sees it, macros are also a major problem for many programming support tools. So when you use macros, you should expect inferior service from tools such as debuggers, cross- reference tools, and profilers. If you must use macros, read the reference manual and try not to be too clever.
Include Guards
#include
will paste the source file in place. But if we include the same file in multiple places we are dulicating the code and so redeclaring the same classes/functions. This gives linker errors (when we combine the compiled .o
files)
So we use macros, to ensure the code is only ‘included‘ once for the whole project. Essentially this makes the file appear empty when pasting with the #include
. The macros below should surround all the code in the file.
#ifndef GRANDFATHER_H //convention: name the symbol as the name of the file
#define GRANDFATHER_H //define the symbol so afterwards we do not enter here
struct foo{
int member;
};
#endif /* GRANDFATHER_H */
Other #include
in the file can be placed anywhere as they are also guarded. Its best to have them below the #define …
as the pre-processor only needs to execute that once.
Examples
Conditional Compilation
#define SYMBOL //or compile with: "c++ -DSYMBOL main.cpp"
#ifdef SYMBOL
something
#else
alternative
#endif
//converted to
something
Replacement
#define A 12
std::cout << A;
//converted to
std::cout << 12;
#define SUM(A,B) A+B
std::cout << SUM(3,4);
//converted to
std::cout << 3+4;
#define std //example of accidental name conflict, avoid this
#define std 1 //equivalent
std::cout << "hello";
//converted to
1::cout << "hello"; //syntax error
Undefine Macros
#define XXX “Hello”
std::cout << XXX;
#undef XXX
std::cout << “XXX”;
//converted to
std::cout << “Hello”;
std::cout << “XXX”;
If-Statement
#if !defined (__GNUC__)
#error This program requires the GNU compilers
#elif __GNUC__>=3
std::cout << “gcc version 3 or higher”;
#endif
2. Make
This is used to compile a project with several files into an executable file, hence a build automation tool. It handles dependencies and detects changes to files, to only recompile necessary files.
- We write a
Makefile
(no file extension) to define how to compile our project in that directory. - We create a rule for each target (output/compiled file), which defines how to create/update that file
- We can add dependencies to a target, this will ensure that each of the dependencies is present and updated (if there is a rule for it) before we run the commands for the target
Use in CLI:
make
usually used to build the whole project. Will build the first target in theMakefile
make mytarget
run the rule formytarget
make -f file
use a specific makefilemake -n
‘dry run’ just prints commands to be executed, useful to debug, see if variables are correct etcmake -C path
run a makefile in the path directory. Used later to run a makefile from within a makefile
2.1 Makefiles
# This is the 'rule' for the output file 'target'
target: dependencie/s
[TAB]command/s
target
the name of the rule and its output filedependencies
list all files that are required to exist/be up-to-date to create the filerule
- Be sure to include the header as a dependency, so that it checks there for changes as well
command
write normal terminal commands, usually to compile acpp
or create alibrary
- Important: needs to have a tab before, not 3 spaces
.PHONY: target
is used to say that a rule does not correspond to a file, e.g.all
orclean
- The target
all
is often added as the first rule which defines what is to be compiled. As it is the first rule, we are setting it as the default target.
- The target
export
to expose variables to the environment. Allows other makefiles called to see the same variables (see longer example)
Short Example:
main.exe: main.cpp simpson.hpp simpson.o
c++ -o main.exe main.cpp simpson.hpp simpson.o
simpson.o: simpson.cpp simpson.hpp
c++ -c simpson.cpp
Instead of issuing commands manually:
$ c++ -c simpson.cpp
$ c++ -o main.exe main.cpp simpson.hpp simpson.o
2.2 Variables
my_var = value
to set variable${my_var}
or$(my_var)
to get value
It is best to create an additional config.mk file to store the local values of the variables (don’t add to git). Then include the file with -include
. By adding the -
in front of include
we say that this is optional.
-include config.mk
Special Variables:
$@
Name of the target, for the current rule$<
Name of the first dependency (like arrow the leftmost dependency)$^
List of all dependencies (with spaces)
Predefined Variables:
CXX
Command for compiling C++ (defaultg++
)
CXXFLAGS
flags/options for the compilerLDFLAGS
flags/options for compilers when linking
LDLIBS
Library flags or names for compilers when linkingMAKE
The make command we used to run the file. Use this when running another makefile from within make, as the options are passed as well. Avoid using just themake
command.RM
Command to remove a file (defaultrm -f
)
2.3 Examples
config.mk, stores variables
CXX = g++
CXXFLAGS = -std=c++11
CXXFLAGS += -Wall -Wextra -Wpedantic
CXXFLAGS += -O3 -march=native
LDFLAGS = -Lintegrator
LDLIBS = -lintegrate
integrator/Makefile, makefile to create the library
# Create a library from the object code
libintegrate.a: simpson.o
ar rvs $@ $^
# ar rvs libintegrate.a simpson.o (evaluated code)
# Object code depending on header and implementation.
simpson.o: simpson.cpp simpson.hpp
$(CXX) $(CXXFLAGS) -c -o $@ $<
# g++ std=c++11 <more flags> -c -o simpson.o simpson.cpp (evaluated code)
.PHONY: clean
clean:
rm -f *.o *.a
Makefile
-include config.mk
# Export variables into the environment. So that other makefiles see the same variables
export
# The first target is called 'all' by convention and used to set what we want to run
.PHONY: all
all: main.exe
# Compile our code and generate an executable binary together with the library
main.exe: main.cpp integrator/libintegrate.a
$(CXX) $(CXXFLAGS) -o $@ $< $(LDFLAGS) $(LDLIBS)
# evaluates to
# c++ -std=c++11 <more flags> -o main.exe main.cpp -Lintegrator -lintegrate
# test yourself with: make -n
# Exported variables will also be visible in a sub-make
integrator/libintegrate.a:
$(MAKE) -C integrator # Call the makefile in the integrator folder
.PHONY: clean
clean:
rm -f *.o *.a main.exe # could also use $(RM) instead of 'rm -f'
$(MAKE) -C integrator clean # call the clean of the other makefile
For projects with multiple directories, it is common to create a Makefile per directory, then call them with $(MAKE) -C path
as in the libintegrate.a
or clean
above
3. CMake
CMake is a cross-platform build system generator. It is used to define how to compile projects. This is so that we can simply specify which executables/libraries we want to create with which files.
Commonly it creates a Makefile
which we can then run to compile our project. However, it can be used for many compilers/platforms/build systems (not just make).
3.1 General
We create a CMakeLists.txt
file where we define how to build our project. Where we write all CMake code
Running CMake
-
cmake <path>
create a Makefile using theCMakeLists.txt
from the<path>
- Reminder: Current directory is
.
and one directory up is..
- The Makefile is created in the current directory by default
- It is recommended to create a separate
build
directory so that our compiled files do not fill up our source directory
- Reminder: Current directory is
make
compile the project with makemake target
compile a specific target (target names defined in CMakeLists)
make install
install files, usually for libraries (defined in CMakeLists what to install)make clean
delete all compiled files
Short Example:
Starting in source directory, where CMakeLists is located
$ mkdir build
$ cd build
$ cmake ..
$ make
or compile a specific target
$ make square.exe
Basic CMake Functions
Short list of basic functions that can be added to the CMakeLists.txt:
cmake_minimum_required()
sets the minimum version of CMake for this project, prevents errors from backward compatibility issues- You should use the current version of CMake on your system
cmake --version
- You should use the current version of CMake on your system
project()
sets the project name, useful for debuggingadd_executable(<Target Name> <Source Files>)
compile an executableTarget Name
name of the executable, use this later when adding libraries etcSource files
list of all files/dependencies, separated with spaces- Source files do not have to be in the current folder, must write with path
add_library(<Name> <TYPE> <Source Files>)
create a library, similar toadd_executable
- Note: The filename has the
lib
prefix (i.e.lib<Name>
) Type
is eitherSTATIC
orSHARED
- Note: The filename has the
target_link_libraries(<target> <libraries>)
Makes the library accessible to the target, adds it in the linking stage of compilation, (-l
flag)- Use the name of the library without the
lib
prefix
- Use the name of the library without the
add_subdirectory(<path>)
run the CMakeLists.txt in a subdirectory, e.g. for your own library- We can then use the targets/libraries created in the other CMakeList
Compiling:
add_definitions (-D<NAME>)
sets compiler definitions for use with macrosinclude_directories(<path>)
Tells the pre-processor where to look for#include <>
files- Use this when header files, e.g. for a library, are not in the default install folders or the project.
- Or to avoid writing path,
#include "QR"
instead of#include "Eigen/QR"
(not recommended)
target_include_directories(<Target> PRIVATE <path>)
Newer version ofinclude_directories
specific for a target and has an access levelfind_package(<NAME>)
searches for an installed library to be linked (see eigen example)add_compile_options(<flags>)
way of setting compile flags manually- e.g.
add_compile_options(-Weverything)
- e.g.
Variables:
set(variable value)
to set the value of a variable, can also have multiple values to create a list$(variable)
to use the variable, as usual
set(CMAKE_BUILD_TYPE Release)
sets release (vs development) to optimise furtherset(CMAKE_CXX_STANDARD 11)
use C++11file(GLOB <variable> <expression>)
finds all files that meet the expression and saves it to the var- e.g.
file(GLOB varName “integrator/*.cpp”)
(not recommended, should be explicit) - Then we can just
add_executable(main ${varName})
- e.g.
Installing Files:
Note: Will only install with make install
The idea is to copy the output files to a standard location so they can be easily accessed from anywhere
e.g. so that we can use the #include <myLibrary>
without having myLibrary
in our project
or run the program from any directory in the terminal
install(TARGETS <targets> DESTINATION <path>)
installs thetarget
outputs (ie.exe
or.a
) by copying them to thepath
- Can specify for only parts to be installed by adding
LIBRARY
,RUNTIME
etc. beforeDESTINATION
FILES
instead ofTARGETS
just copies the files, used for headerspath
will be a relative path, prefix with~
(home) or/
(root) to use the absolute paths
- Can specify for only parts to be installed by adding
Testing:
See Exercise 6 Testing
We create another executable, with a main()
, for the test. When we throw an unhandled exception or an assert is false, the test fails
-
enable_testing()
need to set this otherwise tests will not be compiled -
add_test(NAME test_name COMMAND test_target_name)
to add a target as a test, need toadd_executable(test_target_name test.cpp)
to compile the test target-
This just classifies an executable as a test, so that we can easily call the test and also disable the test from being compiled
-
add_executable(genome_test genome_test.cpp) # create the executable test target_link_libraries(genome_test penna) # add necessary libraries add_test(NAME Genome COMMAND genome_test) # add the test
-
-
make all test
to execute all tests
Minimal Example
cmake_minimum_required(VERSION 2.8) # Require certain version
project(ProjectName) # Name your project
# Compile the square program from the two source files
add_executable(square.exe main.cpp square.cpp)
It is easy to forget to write the VERSION
in the first line
Adding Warning Flags (Common)
if(CMAKE_CXX_COMPILER_ID MATCHES "(C|c?)lang") #need to distinguish between compilers
add_compile_options(-Weverything)
else()
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
3.2 Examples
Libraries with Subdirectories
Generally, we have a separate directory per library. There are two options for subdirectories:
- Use one CMakeLists and add path prefixes, e.g.
timer/timer.h
- Create a separate CMakeLists for each library/subdirectory and ensure it is called with
add_subdirectory(path)
- Normally used for a library or other distinct module of the program
Subdirectories Example (Ex03 Simpson)
We have a subdirectory integrator
with files: CMakeLists.txt, simpson.cpp, simpson.h
Option 1:
cmake_minimum_required(VERSION 3.1)
project(simpson_cmake)
add_library(simpson_lib STATIC integrator/simpson.cpp) # Create the static library
# ensure header files can be included for the simpson.cpp
target_include_directories(simpson_lib PUBLIC integrator)
add_executable(main main.cpp)
target_link_libraries(main simpson_lib) # Link the library to the executable
Option 2:
cmake_minimum_required(VERSION 3.1)
project(simpson_cmake)
# this tells cmake to look for a CMakeLists.txt file in the folder integrator
# (which is where the add_library command is defined)
add_subdirectory(integrator)
# all that remains is to define our executable and the library to link against
add_executable(main main.cpp)
target_link_libraries(main simpson_lib)#Link the library created in the other CMakeList
CMakeLists.txt in integrate/
subdirectory
add_library(simpson_lib STATIC simpson.cpp) # Create the static library
target_include_directories(simpson_lib PUBLIC ./)#Include the headers in the currentdir
Eigen Library
cmake_minimum_required(VERSION 3.10)
project(Exercises)
add_executable(main main.cpp)
# Add Eigen libary, use #include <Eigen/Dense> afterwards
find_package (Eigen3 3.3 REQUIRED NO_MODULE)
target_link_libraries (main Eigen3::Eigen)
Note: this requires that Eigen is installed, i.e. exists in /usr/include/eigen3
sudo apt install libeigen3-dev
Installing Example (Ex06 Benchmark)
Reminder: installing just means copy the files to a specific folder
# Benchmark/CMakeLists.txt
cmake_minimum_required(VERSION 3.1)
project(benchmark)
set(CMAKE_CXX_STANDARD 11)
# If the library is installed in a standard location, we would use find_library
# and not specify this manually
# We choose to do it manually to keep it simpler
if(NOT LIBRARY_INSTALL_PATH) # CMake was not invoked with -DLIBRARY_INSTALL_PATH.
set(LIBRARY_INSTALL_PATH ${CMAKE_SOURCE_DIR}/../install) # hardcode path
endif()
link_directories(${LIBRARY_INSTALL_PATH}/lib) # *Look* for the libraries in that path
include_directories(${LIBRARY_INSTALL_PATH}/include) # Include the library headers
add_executable(benchmark main.cpp)
target_link_libraries(benchmark random timer) # Link the libraries to the executable
install(TARGETS benchmark DESTINATION bin) # Copy the output .exe to the bin/ folder
Random/CMakeLists.txt
, (identical with Timer/CMakeLists
)
cmake_minimum_required(VERSION 3.1)
project(random)
set(CMAKE_CXX_STANDARD 11)
add_library(random STATIC random.cpp) # Create the library
install(TARGETS random DESTINATION lib) #Copy the library output to the lib folder
install(FILES random.hpp DESTINATION include) #Copy the header to the include folder
Note: In this example we need to run cmake and compile the libraries separately, as the benchmark CMake assumes the libraries exist in the lib
folder
4. Classes
Definition of a class (Data declarators)
Classes are collections of “members” representing one entity
Members can be:
- functions
- data
- types
These members can be split into
public
accessible interface to the outside.
Should not be modified later! Only representation-independent interface, accessible to all.private
hidden representation of the concept. Can be changed without breaking any program using the class. Representation-dependent functions and data members.friend
declarators allow related classes access to representation.
Objects of this type can be modified only through these member functions $\implies$ localization of access, easier debugging.
The default in a class is always private.
In a struct the default is public.
How to design classes
What are the logical entities (nouns)?
$\implies$ classes
What are the internal state variables ?
$\implies$ private data members
How will it be created/initialized and destroyed?
$\implies$ constructor and destructor
What are its properties (adjectives)?
$\implies$ public constant member functions u how can it be manipulated (verbs)?
$\implies$ public operators and member functions
Example - Traffic Light
- Property: The state of the traffic light (green, orange or red)
- Operation: Set the state
- Construction: Create a light in a default state (e.g. red) or in a given state
- Destruction: Nothing special needs to be done
- Internal representation: Store the state in a variable
- Alternative: connect via a network to a real traffic light
class Trafficlight {
public:
enum light { green, orange, red };
Trafficlight(light l = red) : state_(l) {} //default constructor
Trafficlight(const Trafficlight& rhs) : state_(rhs.state_) {} //copy constructor
Trafficlight& operator=(const Trafficlight& rhs) {
if (this != &rhs) {
state_ = rhs.get_state();
}
return *this;
}
~Trafficlight(){}
void print_state() const;
light get_state() const { return state_; };
void set_state(light l) { state_ = l; };
private:
light state_;
};
int main() {
Trafficlight a(Trafficlight::green); //green
Trafficlight b(a); //green
Trafficlight* c_ptr = new Trafficlight(Trafficlight::orange); //orange
Trafficlight d = *c_ptr; //orange
Trafficlight e; //red
Trafficlight& f = a; //green
Trafficlight::light l; //create variable of type light
l = Trafficlight::light::red; //change color to red
l = e.get_state(); //assign state of a Trafficlight to light
delete c_ptr;
}
Example - Point
- Internal state: x- and y- coordinates
- Construction: default: (0,0), from x- and y- values, same as another point
- Properties: distance to another point u, x- and y- coordinates, polar coordinates
- Operations: Inverting a point, assignment
class Point {
private:
double x, y;
public:
Point(); //default constructor, (0,0)
Point(double, double); //constructor from two numbers
Point(const Point&); //copy constructor
~Point(); // destructor
double dist(const Point& ) const;
double x() const; double y() const;
double abs() const;
double angle() const;
void invert();
Point& operator=(const Point&);
};
Remember the rule of three: If there is any one of the three following, all three have to be defined:
- Copy Constructor (is automatically generated (a.k.a. synthesized) as memberwise copy, unless otherwise specified)
- Destructor (normally empty and automatically generated)
- Copy Assignment
Nontrivial destructor only if resources (memory) allocated by the object. This usually also requires nontrivial copy constructor and assignment operator. (example: array class)
Class terminology
Declaration
class Point;
Definition
class Point {
private:
double x_,y_;
public:
Point();
void Invert();
...
};
Note: here we have defined the Point
class but only declared its functions
Declaration generally ends in a ;
whereas definition is the {}
part
Implementation
double Point::abs() const {
return std::sqrt(x_*x_+y_*y_);
}
Constructor (ctor)
//preferred method with constructor initializer list, calls the ctors of the members
Point::Point(double x, double y) : x_(x), y_(y) {
...
}
Point::Point() = default; //use compiler generated ctor
5. C++ Features & Keywords
5.1 Features
Namespaces
These are used to organise classes and functions into ‘groups’ to prevent name conflicts (classes with the same name). With a conflict it is often uncertain which is used.
#include <cmath>
namespace MyNamespace {
void sin(float);
}
MyNamespace::sin(1); //call like this
std::sin(1);
sin(1); //uncertain
Can be nested with scope resolution operator ::
e.g. namespace A::B::C {}
Using Namespace:
using namespace std;
Is used to prevent writing the namespace each time, but increases risk of name conflicts.
Important! Do not use this in header files, as it carries over to where it is included in.
We can also just do this for a single function
using std::cout;
now we can writecout
Ternary Operator
Shorthand if-else statement, meant to be used as part of an expression
//expression ? case-when-true : case-when-false
std::log(a >= 0 ? a : -a); //make value absolute
Range-based for loops
Executes a for loop over a range. Works by using the begin()
and end()
of a container. Similar to python for loops
std::vector<double> v(4);
for (double x : v) // for every double x in the container v
std::cout << x; //0000
for (int x : {1, 2, 5})
std::cout << x; //125
for (double& x : v)
x = 5;
Note: if you add/remove elements the iterators can be invalidated so the loop can create errors. Use index based loops when modifying the vector. This happens when the vector has to be resized and copied, so the original pointers are invalid. This is not true for all containers (e.g. lists work fine), see cppref Iterator Invalidation or “container” in index
Lambda Expressions
Form: [value](int x) -> bool {return x > value; };
[…]
is called caption
(…)
are the parameters
-> bool
is the return type
{…}
is the instruction to be done
Capture list:
[x]
x passed by value (copied)[&x]
x passed by reference[&x, y]
Access to reference of x and copied value of y[&]
Default reference access to all objects in the scope of where lambda is declared[=]
Default value access to all objects in the scope of the lambda
5.2 Keywords
Keyword Auto
Type of a variable is implied by the right hand side. Reference and constness works in the same way
std::vector<double> v(5);
auto i = v[3]; //double
const auto& r = v[0]; //const double&
Just need to be careful with templated expressions from optimisation, as we will have an expression type
Typedef / Using
From C++11 using
replaces the old typedef
using element_t = double;
Generally don’t avoid using typedefs. Use them to allow easy modification of your code later on (you only need to change the typedef line and there is no need for refactoring).
Note: the _t
is used to denote types, like uint64_t
.
class Animal {
public:
typedef unsigned short age_t;
age_t age() const;
private:
age_t age_;
};
Constants, Mutable & Volatile
const
Variable, Can only be set once with the constructor
Must use initialiser list when setting constant members of a class
It calls the constructors of the members (hence you should always use the initialiser list to set variables)
struct MyClass {
const int a;
MyClass(int value) : a(value) { //<- initialiser list
a = value; //won't work
}
};
const
Member Functions, prevents modifying of the object inside the function, ie `this` is constant
Only constant functions can be called for constant instances of the class
Note: Calling non-const functions from const functions in the same class is not allowed
double Point::abs() const;
double Point::abs() const {...}
//only const member functions can be called for const objects
const Point P(5,6);
P.invert(); //error: calling non-const function on const object
constexpr
indicates it is possible to evaluate at compile time. Used for both variables and functions Different to const
, which just indicates the value is not changed
Note: constexpr
is written at the front of the declaration. They should be combined for member functions
constexpr void MyFunction();
constexpr void MyMemberFct() const;
mutable
- Allows modification of members even in const objects or inside const functions, “overrides const status”
struct MyClass {
mutable int value;
void func() const {
value++; //this is allowed
}
};
int main(){
const MyClass a;
a.value++; //allowed
}
volatile
- Tells compiler value may change between every access (read or write) or cause side-effects
Prevents some optimisations
Static Variables
Variables persist through the whole program, are not deleted at the end of the scope
Can be used to prevent a variable from being reinitialised, e.g. in a loop or function
Note: The variable will still only be accessible inside its scope, as usual
foo() {
static int count = 0; //only executed the first time
count++; //executed every time, value persists from last call
}
std::cout << count; //error: count only accessible in its scope
for(int x = 0; x < 10; x *= 2) {
static int number_of_times = 0;
number_of_times++;
}
Static Inside Classes
Variable/Function exists once for all instances, does not require an instance of the class to exist
Accessed through class MyClass::myVar
or directly if inside the class
class Genome {
public:
static const unsigned short gene_number;
Genome clone() const; //non-static
static void set_mutation_rate (unsigned int value){
mutation_rate = value;
}
private:
unsigned long gene_;
static unsigned int mutation_rate;
};
int main(){
Genome::gene_number = 128;
Genome::set_mutation_rate(3);
}
Friends
Allowing classes to have access to each others internal representation (private parts).
class Vector;
class Point {
//...
private:
double x,y,z;
friend class Vector;
};
class Vector {
//...
private:
double x,y,z;
friend class Point;
};
This also works for functions
//written in another class (not Point)
friend Point Point::invert(...); //allow the invert function to access privates
friend int func(...);
friend Point::Point(); //make a ctor a friend
*this
this
is a pointer to the instance with which a member function was called
It is available in all non-static member functions
double Point::x() const {
return this->x; //or (*this).x or simply x
}
//array copy assignment
const Array& Array::operator=(const Array& arr) {
//copy the array
return *this; //return the object with which the function was called
}
int main(){
Array a;
a = ...; //now in operator= *this is a
a = b = c = d; //possible as we always return the instance so it can be chained
a = (b = (c = d)) //where (c = d) return c
}
=default & =delete
These are used to use/prevent compiler generated functions, often used for ctors/dtors, operators
class Point{
Point() = default; //shows generate ctor is used
~Point() = delete; //we must implement our own dtor
Point operator=(Point&&) = default; //use generate move assignment
};
Misc
-
decltype(variable)
returns the type of the variable or expression, useful with templatingstd::complex<double> cd; decltype(cd) other; //other has the same type as cd
-
explicit
function: Prevent implicit conversions on the input, types must match the parameters exactly -
Const Pointers:
T* const ptr;
$\implies$ pointer itself can’t change value of address it stores (similar to reference)const T* ptr;
$\implies$ pointer can move but the integer value must stay the same- Note left of the
*
is the type we are pointing to
-
Cast-conversion: allows compiler to convert the type of a variable if it knows how to
- static casting: explicit casting, always use static casting in exam!
static_cast<to_type>(from)
-
Encapsulation: abstraction of use from the implementation, getters/setters
6. Functions & Operators
Inlining
Copies the code of the function and pastes it where it is called, function does not exist after compilation.
Can improve performance if function is called often.
Avoid excessive inlining as it leads to large executable file. Try to avoid with larger functions.
Note that the keyword inline
is only a suggestion to the compiler. For higher compiler optimisation levels, some functions will be inlined even without the inline
keyword.
When to use:
- Small functions, saves having to rewrite each time, often used for getters/setters
- Large functions, if they are only called once or so (can be used for better organisation)
class complex {
private:
double re_, im_;
public:
inline double real() const;
inline double imag() const;
};
double complex::real() const {
return re_;
}
double complex::imag() const {
return im_;
}
Functors & Passing Functions
See Exercise 8, Simspson Benchmark, esp. simpson.hpp in benchmark
Functions can be passed by:
-
Functor/Function Object
- Class with overloaded
operator()
- Pass instance of function
- Class with overloaded
-
Function Pointer
- Pass a function directly as a parameter
- Create function pointer:
return_type (*myfunc) (param1, param2)
- For member functions use
std::mem_fn
, needs to be wrappedauto greet = std::mem_fn(&Foo::display_greeting); greet(f);
- Or
std::function<return_type (param1, param2)>
(advanced function pointer) - Use
auto
These can be made generic with templates or abstract base class functors
Operators
See Week 4 Slide 31 for all operators and member vs non-member operators
See Week 0506a Slide 51 for rule of 3/5
class Inverse {
double operator() (double d) { //overload call operator
return 1.0 / d;
}
};
Inverse V;
std::cout << V(5.0);
Point Example
struct Point {
double x, y;
const Point& operator+=(const Point& rhs) {
x += rhs.x;
y += rhs.y;
return *this;
}
Point operator+(const Point& other) {
Point tmp{x + other.x, y + other.y};
}
};
//stream operators usually non-member functions
std::ostream& operator<<(std::ostream& out, const Point& p) {
out << "(" << p.x << ", " << p.y << ")";
return out;
}
//we can now use print:
int main(){
Point p1, p2;
std::cout << "The point is " << p1 + p2 << std::endl;
}
Which Operator to overload
A a; ++a; //requires either
const A& A::operator++(); //operator as member
const A& operator++(A&); //or as a non-member
A a; a++; //requires
A A::operator++(int);
//The additional int argument is just to distinguish the pre- and postincrement
A b = a; A b(a); //both use the copy constructor
A::A(const A&);
Conversion Operators
Type conversion of A $\rightarrow$ B as in:
A a; B b=B(a);
//can be implemented in two ways:
B::B(const A&); //constructor
A::operator B(); //conversion operator
Keep in mind the implicit type conversion order:
i) bool, char < int < unsigned int < float < double
ii) short < int < long < long long
7. Traits
Also see http://blog.aaronballman.com/2011/11/a-simple-introduction-to-type-traits/
Recall the template for the min function
template <typename T>
T const& min(T const& x, T const& y) {
return x < y ? x : y;
}
We now want to allow things like min(1.0,2)
and min(1,2.0)
Now, instead of doing it manually with 3 different typenames and having to call the function with the return type min<int>(1.0,2)
or min<double>(1,2.0)
, we use traits.
template <class T, class U>
typename min_type<T,U>::type& min(T const& x, U const& y)
The keyword typename is needed here so that C++ knows the member is a type and not a variable or function. This is required to parse the program code correctly – it would not be able to check the syntax otherwise.
How to derive min_type:
//empty template type to trigger error messages if used
template <typename T, typename U>
struct min_type {};
//partially specialized valid templates:
template <class T>
struct min_type<T,T> {typedef T type; };
//fully specialized valid templates:
template <>
struct min_type<double,float> {typedef double type; };
template <>
struct min_type<float,double> {typedef double type; };
template <>
struct min_type<float,int> {typedef float type; };
template <>
struct min_type<int,float> {typedef float type; };
//add more specialized templates here
template <typename T>
struct average_type {
typedef typename
helper1<T,std::numeric_limits<T>::is_specialized>::type type;
};
//the first helper:
template<typename T, bool F>
struct helper1 {typedef T type;
};
//the first helper if numeric limits is specialized for T (a partial specialization)
template<class T>
struct helper1<T,true> {
typedef typename
helper2<T,std::numeric_limits<T>::is_integer>::type type;
};
//the second helper
template<class T, bool F>
struct helper2 {
typedef T type;
};
//the second helper if T is an integer type (a partial specialization)
template<class T>
struct helper2<T,true> {
typedef double type;
};
C++ Concepts
A concept is a set of requirements on types:
- The operations the types must provide
- Their semantics (i.e. the meaning of these operations)
- Their time/space complexity
A type that satisfies the requirements is said to model the concept. A concept can extend the requirements of another concept, which is called refinement. The standard defines few fundamental concepts, e.g.
- CopyConstructible
- Assignable
- EqualityComparable
- Destructible
8. Exceptions
Overview:
- Use asserts when testing your code, check errors coming from your own code or for high performance
- Use exceptions when writing code that is used by others, when the error can be resolved/handled, you want to give more data on the error
Exceptions enable good error handling, especially with use of libraries or functions. There are some more primitive ways of handling errors than exceptions:
- Global error variables/Flags (not recommended)
- Functions returning error codes or objects
Global variables: Typical for older C-code,
Set a global variable when an error occurs, then check at other places if the error has occurred.
This typically fills the code with a lot of seemingly random checks. Cannot call in parallel.
Functions returning error code: Typical for big APIs (OS level or http),
Functions exit when an error occurs and return a ‘status’ variable. Then check the status code with if-statements at the place of calling, instead of try-catch.
Problems:
- Makes it harder to return other values or give detail on the error.
- Need to handle deleting of memory manually
Asserts terminate the program if a condition is not met,
These should only be used for internal testing, when something is violated that means the program cannot operate. However, these are good when testing your own code. Asserts can be turned off for performance, use the -DNDEBUG
compile flag. E.g. checking invariants in your algorithm, necessary pre-conditions for internally used functions.
Exceptions: Change the control flow,
Exceptions ensure the function is left safely (deletes allocated memory) with an error message and allows handling of the error. It will keep exiting the functions up the stack until it is caught (works on multiple levels of function calls). Exceptions can hence be used to print a stack-trace, to see where the error occurred and what led to it.
How to use:
try
will attempt to execute the enclosed code, when an exception is thrown the scope is exited safely and can be caught. It will not execute the code in thetry
scope below where the error occurred.catch(Type e)
written after atry
if we know how to resolve a particular type of error. Write one per type of errorcatch(...)
Catch all exceptions, should be the lastcatch
. We do not have a parameter as we don’t know the type of what we are catching. It is usually a sign of a fatal error.throw <object>
to signal an error an leave the function. We can re-throw the error in a catch. Can throw any type, butstd::exception
is recommended.- Use
e.what()
to get the message from an error (namede
)
When not to use:
- Do not use exceptions to change the control flow of your program.
- Avoid exceptions in destructors
Example
void my_function(){
if(n<=0)
throw "n too small"; //throwing a char array
if(index >= size)
throw std::range_error("description");
}
try{
my_function(); //only put code in here which is likely to throw an exception
}
catch(std::logic_error& e){ //Handle a logic_error
std::cout << e.what();
throw e; //re-throw error like this
}
catch(...){ //catch all other error types
std::cerr << "A fatal error occurred.\n";
}
Standard exceptions are in <stdexcept>
, derived from std::exception
.
(<exception>
is part of <stdexcept>
and only contains the base class std::exception
)
Logic errors (base class std::logic_error
) error due to incorrect semantics. Violation in pre-condition, invariants
domain_error
invalid_argument
length_error
out_of_range
Runtime errors (base class std::runtime_error
) error beyond what the function was intended to handle
range_error
an invalid value occurred as part of a calculationoverflow_error
a value got too largeunderflow_error
a value got too small
9. Timer
The standard library contains:
- Header
<ctime>
: date & time - Header
<chrono>
(since C++11): precision time measurements
Benchmarking example:
//requires #include <chrono>
std::chrono::time_point< std::chrono::high_resolution_clock > t_start, t_end;
t_start = std::chrono::high_resolution_clock::now();
//put code to benchmark here
t_end = std::chrono::high_resolution_clock::now();
std::cout << "It took: " << static_cast<std::chrono::duration<double> >(t_end - t_start).count()
<< " seconds." << std::endl;
10. Random Number Engines
Random numbers
Real random numbers: are hard to obtain. It can be done from physical phenomena
Pseudorandom numbers:
- Get random numbers from an algorithm
- Totally deterministic and therefore not random at all, useful for debugging
- But maybe good enough for your application
- Never trust (just one) (pseudo) random number generator
Utilities for dealing with random numbers in standard library since C++11: Header <random>
Useful and good generators:
#include <random>
//Mersenne-twisters
std::mt19937 rng1;
std::mt19937_64 rng2;
//lagged Fibonacci generators
std::ranlux24_base rng3;
std::ranlux48_base rng4;
//linear congruential generators
std::minstd_rand0 rng5;
std::minstd_rand rng6;
//set the seed of a RNG
#include <random>
//default random engine
std::default_random_engine e;
e.seed(42); // set seed
Distributions
Uniform distributions:
- Integer:
std::uniform_int_distribution<int> dist1(a,b)
- Floating point:
std::uniform_real_distribution<double> dist2(a,b)
Exponential distribution:
std::exponential_distribution<double> dist3(lambda);
Normal distribution:
std::normal_distribution<double> dist4(mu, sigma);
11. Data Structures in C++
Arrays
Are consecutive range in memory. Fast arbitrary element access. Profits from cache effects. Constant time insertion and removal at the end. Searching in sorted array is O(ln N). Insertion and removal at arbitrary position in O(N).
- C array
- vector
- valarray
- deque:
The deque is more complicated to implement, but yields constant time insertion and removal at the beginning.
Linked lists
- list:
A linked list is a collection of objects linked by pointers in sequence. Constant time insertion and removal anywhere (just reconnect the pointers). Does not profit from cache effects. Access to an arbitrary element is O(N), searching is also O(N).
Functions:splice
joins lists without copying, moves elements from one to end of the other.sort
optimized sort, just relinks the list without copying elementsmerge
preserves order when “splicing” sorted listsremove(T x)
remove_if(criterion)
criterion is a function object or function, returning a bool and taking a const T& as argument.
Example:bool is_negative(const T& x) { return x<0; }
list.remove_if(is_negative);
Trees
- map
- set:
Unordered container, entrys are unique. - multimap:
Can contain more than one entry (e.g. phone number) per key. - multiset:
Unordered container, multiple entries possible
Queues and Stacks
- queue:
The queue works like a Mensa, FIFO (first in first out). In constant time you can push an element to the end of the queue, access the first and last element and remove the first element.
Functions:void push(const T& x)
//inserts at endvoid pop()
//removes frontT& front()
,T& back()
,const T& front()
,const T& back()
- priority_queue:
The priority_queue is like a Mensa, but professors are allowed to go to the head of the queue (not passing other professors!). The element with the highest priority is the first one to get out. For elements with equal priority, the first one in the queue is the first one out. Prioritizing is done with < operator. - stack:
The stack works like a pile of books, LIFO (last in first out). In constant time you can push an element to the top, access the top-most element and remove the top-most element.
Functions:void push(const T& x)
//insert at topvoid pop()
//removes topT& top()
const T& top() const
Generic traversal of containers
We want to traverse a vector and a list in the same way:
for (auto p = a.begin(); p != a.end(); ++p)
cout << *p;
//Specific to Array<T> (replace T if necessary)
for (typename Array<T>::iterator p = a.begin(); p != a.end(); ++p)
cout << *p;
Note: We can replace auto
with the containers iterator
, but this makes it less generic
Array implementation:
template<class T>
class Array {
public:
typedef T* iterator;
typedef unsigned size_type;
Array();
Array(size_type);
iterator begin(){
return p_;
}
iterator end() {
return p_+sz_;
}
private:
T* p_;
size_type sz_;
};
Linked list implementation:
template <class T>
struct node_iterator {
Node<T>* p;
node_iterator(Node<T>* q) : p(q) {}
node_iterator<T>& operator++() {
p=p->next;
return *this;
}
T* operator ->() {
return &(p->value);
}
T& operator*() {
return p->value;
}
bool operator!=(const node_iterator<T>& x) {
return p!=x.p;
}
// more operators missing ...
};
template<class T>
class list {
private:
Node<T>* first;
public:
typedef node_iterator<T> iterator;
iterator begin() {
return iterator(first);
}
iterator end() {
return iterator(0);
}
};
12. Algorithms Overview
Mostly found in <algorithm>
header, some also from cmath
, numeric
See ‘Algorithms Library’ in cppreference!
Example - linspaced vector (1-10), square each element, shuffle vector, copy range, print each element
#include <algorithm> //for_each, transform
#include <numeric> //iota
int main() {
std::vector<int> v(10);
std::iota(v.begin(), v.end(), 1); //start 1, spacing 1 to get (1, 2, 3...10)
auto squarelam = [](int x){ return x*x; }; //square the element
std::transform(v.begin(), v.end(), v.begin(), *squarelam); //*apply* for each
//v = 1 4 9 16 25 36 49 64 81 100
std::random_shuffle(v.begin(), v.end()); //randomise v
std::vector<int> v1(5);
std::copy_n(v.begin() + 2, 5, v1.begin()); //copy elements [2, 2+5) to v1
auto printlam = [](int x){ std::cout << x << " "; };
std::for_each(v1.begin(), v1.end(), *printlam); //call print for each element
//output: 9 16 25 36 49
std::for_each(animals.begin(), animals.end(), std::mem_fn(&Animal::grow));
//use mem_fn if the function is a member
//required as else we do not know what grow is, could be a var
}
Non-modifying:
- for_each(begin, end, function)
- find, find_if, find_first_of
- adjacent_find
- count, count_if
- mismatch
- equal
- search
- find_end
- search_n
Modifying:
- transform(begin, end, output begin, function)
- copy, copy_backward
- swap, iter_swap, swap_ranges
- replace, replace_if, replace_copy, replace_copy_if
- fill, fill_n
- generate, generate_n
- remove, remove_if, remove_copy, remove_copy_if
- unique, unique_copy
- reverse, reverse_copy
- rotate, rotate_copy
- random_shuffle
Sorted sequences:
- sort, stable_sort
- partial_sort, partial_sort_copy
- nth_element
- lower_bound, upper_bound
- equal_range
- binary_search
- merge, inplace_merge
- partition, stable_partition
Permutations:
- next_permutation
- prev_permutation
Set Algorithms:
- includes
- set_union
- set_intersection
- set_difference
- set_symmetric_difference
Minimum and Maximum:
- min
- max
- min_element
- max_element
- lexicographical_compare
13. Templates
Function Overloading
We can declare functions with the same name if they have a different number/types of parameters.
The compiler will choose the matching version of the function
int min(int a, int b) { return a<b ? a : b; }
double min(double a, double b) { return a<b ? a : b; }
min(1,3); //calls min(int, int)
min(1.0,3.0); //calls min(double, double)
Problems arise with implicit conversions
min(1, 3.14); // Problem! which type?
min(1.0, 3.14); // OK
min(1, int(3.14)); // OK but does not make sense
// could also define new function "double min(int,double);"
During compilation, this works as they don’t really have the same name. The function argument types are appended to the function name, e.g. our integer min is _Z3minii
(i
for integer)
Note: For generic functions use templates.
This is more used when there are different number of arguments, but it may be best to use default argument values, i.e. int min(int a, int b = 0);
Generic Algorithms
We can implement the example above generically by using a template:
template <typename T>
T const& min (T x, T y)
{
return (x < y ? x : y);
}
What happens if we want to use a mixed call in the example above min(1,3.141)
?
$\rightarrow$ Now we need to specify the first argument since it cannot be deduced
min<double>(1,3.141);
min<int>(3,4);
Advantages of using templates are, that we get functions that:
- work for many types T
- are as generic and abstract as the formal definition
- are one-to-one translations of the abstract algorithm
Templated Classes
template <typename T>
class A {
public:
typedef T T2; //alternate name for T
A(); //constructor
//...
T func1(T x) {return x; } //definition
T func2(T); //declaration only
private:
//...
};
//define a member outside of the class body:
template <typename T> T A::func2(T x) {
//...
}
template <typename T>
class sarray {
public:
typedef std::size_t sz_t; //size type
typedef T elem_t; //element type
// ... as before
private:
sz_t size_; //size of array
elem_t* elem_; //pointer to array
};
std::size_t
is simply an unsigned integer type that can store the maximum size of a theoretically possible object of any type.
If we want to create a fully compatible array class, we use typedef to create our generic types:
template <typename T>
class sarray {
public:
typedef std::size_t size_type; //size type
typedef T value_type; //value / element type
typedef T& reference; //reference type
typedef T const& const_reference; // const reference type
// ...
private:
size_type size_; //size of array
value_type* elem_; //pointer to array
};
Now we need to add the keyword typename
to our operator overloads, or the compiler won’t know that the member is a type and not a variable or function.
// subscript operator
template <typename T>
typename sarray<T>::reference sarray<T>::operator[](size_type index) {
assert(0 <= index && index < size());
return elem_[index];
}
// const subscript operator
template <typename T>
typename sarray<T>::const_reference sarray<T>::operator[](size_type index) const {
assert(0 <= index && index < size());
return elem_[index];
}
Template Specialization
Take as an example an array of bools. Internally it is stored as one byte for every entry. We want to specialize this such that every bool only takes up one bit of storage.
template <class T>
class sarray {
//generic implementation
//...
};
template <>
class sarray<bool> {
//optimized version for bool
...
};
14. Inheritance
See Exercise 9 Inheritance
- Store shared behaviour/variables in a base class
- Can then create derived classes to extend the base class for a more specific case
- Form of Polymorphism, can look at instances in a generic sense, but treat them differently
- We can have a vector of the base class, but actually have a vector of varying types
Properties:
virtual
allows the function to be overriden (modified) by derived classes- For derived instances the derived function will be called
override
not required but enables compiler checks to check for a matching virtual function- Only a matching declaration is needed to override a virtual function
- Call base class function from the derived class equivalent with the scope modifier
BaseClass::func()
- The function remains virtual for a class deriving from the derived class, sticky
- Can inherit from any class and have multiple base classes
struct Animal{
double age;
virtual void Eat();
};
struct Fish : public Animal {
void Swim();
void Eat() override; //modifies the base function
};
struct Bird : public Animal {
void Fly();
};
Here Fish
, Bird
are derived classes and Animal
is the base class.
What is not inherited:
- Anything Private
- Con/Destructors, Assignment operators
- Friends
Access Specifiers
We can set access levels of variables and functions. These define who can read/write/call the variables/functions
Important: Derived classes will still inherit all the functionality and data
- public anyone can access (read/write/call)
- protected only accessible to derived classes
- private only the class itself has access
- Instances of the derived class will have these variables/functions, but they are not accessible directly or via member functions of the derived class. They can only be seen by the member functions of the base class.
We can set also an access specifier on the inheritance itself. This sets the max access level of the inherited variables/functions
The default access specifier for the base class is the same as the default for class
(i.e.private
) or struct
(i.e.public
). Usually we want to inherit publicly, as we want to have the same access levels as the base class
Example:
class A
{
public:
int x;
protected:
int y;
private:
int z;
};
class B : public A
{
// x is public
// y is protected
// z is not accessible from B
};
class C : protected A
{
// x is protected
// y is protected
// z is not accessible from C
};
class D : private A // 'private' is default for classes
{
// x is private
// y is private
// z is not accessible from D
};
Note: Classes B, C, D all contain the variables x, y, z. It is just question of access, can we modify/read
Example from https://stackoverflow.com/a/1372858/9295437
Abstract Base Classes (ABC)
Prevent instances of the base class itself by forcing overriding of functions for derived classes
Using = 0
for a function forces the derived class to override the function. Makes the class abstract
Important: Must pass ABC by reference/pointer, as pass by value creates a copy which is not allowed
#include <iostream>
struct MyBase{
virtual void print() const{}
virtual void run() =0; //must be overriden, defined in derived class
};
struct Derived: public MyBase {
void print() const override{
//extra functionality
MyBase::print(); //call base function of run, works as print is not abstract
}
void run() override {}
};
struct OtherDerived: public MyBase{
//print() does not have to be overriden
void run() override{}
};
void Func(MyBase& b){ //must not pass by value, else compiler error
b.print(); //will call the derived version
}
int main(){
MyBase b; //Compiler error: class is abstract
Derived a;
a.run(); //calls Derived::run
}
15. Programming Styles
Run-time vs Compile-time Polymorphism
Run-time (Dynamic) | Compile-time (Static) |
---|---|
Uses virtual functions (Inheritance) | Uses templates |
Decision at run-time | Decision at compile-time |
Works for objects derived from the common base | Works for objects with the right members |
One function created for the base, class $\implies$ saves space | A new function is created for each class used $\implies$ takes more space |
Virtual function call needs lookup in type table $\implies$ slower | No virtual function call, can be inlined $\implies$ faster |
Extension possible using only definition of base class | Extension needs definitions and implementations of all functions |
Most useful for application frameworks, user interfaces, “big” functions | Useful for small, low level constructs, small fast functions and generic algorithms |
Comparison on programming Styles
The following chapter will show the different advantages and disadvantages of programming styles by means of the implementation of a stack
Procedural programming
Procedural programming is one of the more simple programming paradigms. It is used in many different programming languages.
See simpson integration in Ex 1.6 for more details
void push(double*& s, double v){
*(s++) = v;
}
double pop(double *&s) {
return *--s;
}
int main() {
double stack[1000];
double* p = stack;
push(p,10.);
std::cout << pop(p) << “\n”;
std::cout << pop(p) << “\n”;
// error of popping below
// beginning goes undetected!
}
Modular programming
The modular implementation of a stack allows transparent change in underlying data structure without
breaking the user’s program.
See simpson integration in Ex 2.2 for more details
namespace Stack {
struct stack {
double* s;
double* p;
int n; };
void init(stack& s, int l) {
s.s=new double[l];
s.p=s.s;
s.n=l; }
void destroy(stack& s) {
delete[] s.s; }
void push(stack& s, double v) {
if (s.p==s.s+s.n-1) throw
std::runtime_error(“overflow”);
*s.p++=v; }
double pop(stack& s) {
if (s.p==s.s) throw std::runtime_error(“underflow”);
return *--s.p; }
}
int main() {
Stack::stack s;
Stack::init(s,100); // must be called*
Stack::push(s,10.);
Stack::pop(s);
Stack::pop(s); // throws error
Stack::destroy(s); // must be called
}
Object orientated programming
By implementing the stack as a class, one is able to encapsulate the data and make use of the automatic initialisation and cleanup of the class.
See simpson integration in Ex 4.2/5.3 for more details
namespace Stack {
class stack {
double* s;
double* p;
int n;
public:
stack(int=1000); // like init
~stack(); // like destroy
void push(double);
double pop();
};
int main() {
Stack::stack s(100); // initialization done automatically
s.push(10.);
std::cout << s.pop(); // destruction done automatically
}
Generic programming
By templating the class and implementing it generically you can ensure that the stack works for any data type. It also creates an efficient datatype because of the fact that the instantiation happens at compile time.
See simpson integration in Ex 8.1 for more details
namespace Stack {
template <class T>
class stack {
T* s;
T* p;
int n;
public:
stack(int=1000); // like init
~stack(); // like destroy
void push(T);
T pop();
};
int main() {
Stack::stack<double> s(100); // works for any type!
s.push(1.3);
std::cout << s.pop();
}
16. Hardware
We need to understand hardware optimisations and limitations so we know how to optimise in C++
16.1 CPUs
Basic components of the CPU:
- Memory controller: Manages loading from and storing to memory
- Registers: Fastest memory storage inside CPU, very small, can store integers, floating point numbers, specified constants
- Arithmetic and logic units (ALU):
- Perform arithmetic operations and comparisons
- Operates on registers (fast)
- On some CPUs can directly operate on memory (slow)
- Fetch and decode unit: Fetches instructions from memory, interprets the numerical value, decides what to do and sends them to the ALU or memory controller
Instruction Sets
Every processor has a fixed instruction set determined by the hardware, essentially a list of all fundamental operations it can do. Every program uses only these operations, which can be seen in the assembly code.
There are two alternatives, either to have a large instruction set (CISC) with lots of possible operations. Or create everything from a small set of instructions (RISC). Both instruction sets can perform any operation/program and can be considered equivalent, depends on usage case.
CISC: Complex Instruction Set Computer
Instruction set that implements many high level instructions (e.g sin, cos). They usually have high clock cycles but take many cycles to complete instructions
RISC: Reduced Instruction Set Computer
Use low level instructions to compute everything. They execute instructions very quickly and can be pipelined well, but need a large amount of assembly instructions to create programs $\implies$ More memory usage
Von Neumann Bottleneck
A computers ‘speed’ is always limited by the slowest component, called bottleneck. The CPU cannot perform operations if it does not have the data for it. Usually this bottleneck is loading data from memory or the hard-drive into the CPU, called the von Neumann bottleneck
This is important as it is often what we need to optimise the most.
Developments over time
Processor speeds have increased significantly. Memory improvements have mostly been in density – the ability to store more data in less space – rather than transfer rates.
As speeds have increased, the processor has spent an increasing amount of time idle, waiting for data to be fetched from memory. No matter how fast a given processor can work, in effect it is limited to the rate of transfer allowed by the bottleneck, the whole system needs to be fast.
16.2 CPU Optimisations
Specifically Single Core
Beware that CPUs will further optimise the binary machine code to execute it as fast as possible, these are some of the tricks it does or can do.
They are based on: utilising all resources fully (pipelining), predicting (branch prediction) and Instruction Level Parallelism ILP (Pipelining/Vectorisation)
These optimisations are related to processing, but there are also memory optimisations with similar concepts, e.g.
Predicting - temporal locality (keep newest in cache) and spatial locality/pipelining (read cache line)
Parallelisation - using a bus, read multiple data at once
Pipelining
A form of ILP
Pipelining is the process of increasing performance by running consecutive independent instructions before the previous ones are finished in the same processor core (i.e. without threading). CPUs do this with their circuits, they send input signals into the circuit before the previous signal has finished. Only works if we structure our code correctly.
Loop unrolling is a way of making the operations independent, allows for pipelining to happen. But branch prediction is also used for this…
Branch Prediction
Pipelining is not possible if there are branches in the program (if-statements), since we need to know the output before starting the next operation.
CPUs circumvent this problem by predicting/guessing the result of the branches and simply starting the execution of the most likely branch (e.g. for loop, continuing the loop).
If predicting is correct, then the pipeline continues as normal but has saved time. Else, if the prediction is wrong, the pipeline aborts and starts again at the right branch.
Sometimes even both branches are computed and the wrong one is discarded.
Vectorisation
Various Names/Versions: MMX, SSE, SIMD(Single Instruction Multiple Data), AVX
Also a form of Instruction Level Parallelism (ILP)
Vector operations perform the same operation on multiple values at the time. Typical operations are elementwise addition/multiplication on vectors and matrices. The compiler will attempt to vectorise (–ftree-vectorize
flag), but it is best to do it manually
GPUs
GPUs are specifically designed for performing vectorised (SIMD) instructions.
They have many cores (100-3000) but the cores are less complex, and can only do basic operations. Less for logic, more for lots of data. They are more difficult to program due to parallelisation, need to use other languages e.g CUDA, OpenCL, CG. Sharing memory between CPU and GPU is also a problem.
16.3 Memory
As faster memory is expensive, we have a hierarchy of different sized memory to prevent the CPU waiting for data. At the top is the storage actually used by the processor (registers). The less memory a program needs, the faster it runs as we can fit most of the program into a higher cache (not necessarily faster, but reduces the memory bottleneck)
We can see how there is a slowdown when each cache level is full:
Random Access Memory (RAM)
Note: We often ambiguously use ‘memory’ to refer to RAM
There are many different types of varying size, price and speed.
- SRAM: static RAM, very fast, expensive, data stored on flip-flops, data stays as long as there is power
- DRAM: dynamic RAM, slower, much cheaper than SRAM, data stored on tiny capacitors, capacitors need to constantly be recharged
- SDRAM: synchronous dynamic RAM, synchronised to cache cycle, faster than DRAM
- DDR RAM: double data rate RAM, can send data twice per clock cycle
- Interleaved Memory Systems: uses multiple memory banks
- Can read from all simultaneously $\implies$ same latency, more band width
Caches
As the bottleneck is usually reading data from the hard-drive/memory, we cache data read in several levels (L1-L3). The CPU will check if the data it needs exists in the memory hierarchy from the top downwards. When checking in a level, if the data exists it is called a cache hit (else cache miss and the search continues, slower)
16.4 Memory Optimisations
Two Assumptions:
- Temporal Locality - Data tends to be reused, data currently being used will be used in future.
- So we keep data in the cache until it is full
- We discard oldest data first
- Spatial Locality - Data needed next is near the data currently being used
- i.e. data use is chronologically ordered, e.g. arrays
- Read data surrounding data as well $\implies$ cache line
Note: These optimisations occur between/on levels of the cache. Test values with $ getconf -a
Cache Line
When reading data into a higher level in the memory hierarchy, there is always an initial delay. So it is best to read more data at once and request less often. Due to spatial locality we read the data requested plus data around it in a block. This block has a fixed size called cache line size. Each level has its own cache line size but they are usually equal.
When the CPU reads data from memory, a full cache line is loaded into the L3 cache and further up the memory hierarchy. This means that the data itself and more data sequentially after it is in the cache. So if we need the surrounding data next, we saved the initial delay as it is already in cache.
So in C++ we should organise the data/usage so it is reads sequentially, so the data read in the cache line is always used. E.g. matrix row vs column major needs to match in matrix storage and reading sequence.
Note! 64/32-bit is not related to the cache line size, it is about the number of bits used for addresses in the memory.
Cache Associativity
Cache associativity is how data is mapped from a larger to a smaller sized storage, similar to hashing. Direct mapping is the cheapest as it requires least complex calculations (and so circuits).
- Direct mapped - Each memory location can only be stored at one cache location.
- Address in cache can be calculated with modulo, $index_\mathrm{cache} = index_\mathrm{memory} \% size_\mathrm{cache}$
- n-way associative - Each memory location can be stored at n cache locations
- E.g. does a similar modulo, then a linear search in the next n elements for the oldest/empty location
- Fully associative - Each memory location can be stored at any cache location
- Performs a linear search through the whole cache
Due to temporal locality, if an index is full, we discard the oldest.
Note: It is often easier to look at the n-way associativity as a grid, with y-axis being the ‘Index’, x-axis the ‘Way’. The number of ways is the n.
An address is assigned to an index and does search through all the ways in that index.
Cache associativity is useful as it allows us to rearrange the data from the cacheline and prevent cache thrashing, which is when we constantly replace data we are using. Means we have to re-read each time, defeating the concept of caching.
Cache thrashing is the worst when the stride size equals the cache line size. Larger strides may improve performance, due to the modulo. With higher associativity we have this less.
E.g. in image above, what happens if we compare every 4th element and have a cache line size of 4. We will always fully replace the previous data the cache and have to re-read each time. With 2-way we will always have the last 2 in cache, for full-associativity we will have the last 4.
Virtual Memory
The abstraction of mapping logical memory onto physical memory addresses. Handled by hardware+OS
- Processes run independently, for each process it appears it is running on its own
- Prevents memory conflicts/overlaps between processes
- We can extend RAM when full with the slower hard-drive
- Prevents crashing when memory is full
- The mapping from virtual to physical memory addresses is stored in page tables in the RAM
- To get data we need to first read address from page table $\implies$ slow
- So we cache the page table, called Translation Lookaside Buffer (TLB)
Note: Does not apply to caches, only to RAM. When the caches are full we replace old data
Worst Case Caching
Always try to reuse data as much as possible. Worst case when fetching data:
- Request an address
- Cache miss in L1-3
- Lookup physical address
- Cache miss in TLB (translation lookaside buffer)
- Request page table entry
- Load page table from memory (slow)
- Page fault in page table
- Store a page to disk (extremely slow)
- Create and initialise a new page (very slow)
- Load page from disk (extremely slow)
- Load value from memory (slow)
17. Optimisation
Do easy and significant optimisations first
Optimal approach to optimisation:
- Use compiler flags (see section 1 C++ compiling)
- Use profiling to determine slow parts of program
- Find best data structure & algorithm and a library which implements it
- Look into more advanced optimisations
- Inlining
- Read data in a sequential order, to use data in cache line effectively
- Unroll loops
- Vectorisation/SIMD (single instruction multiple data e.g. MMX, AVX)
- Lazy evaluation and Expression Templates, compile time calculations
- Use processor specific intrinsics
17.1 Profiling Runtime
- Time program in CLI with
time
- More detailed profile with
gprof
, to see calls and runtime of individual functions - Time inside C++ with
chrono
(Manual Instrumentation) - Use a more advanced profiler, e.g.
perf
or vtune, or also profile memory usage etc
A. time
is a simple shell command which returns the runtime of a program. It is the simplest way to time your program.
You can add the option -p
in order to get the time in seconds
$ time program.exe
real 0m0.872s # effective total time
user 0m0.813s # cputime of the program
sys 0m0.016s # cputime of other software whilst the program is running.
B. gprof
can profile C++ more exactly, timing individual functions
- Compile the program with
-pg
- Run it, this creates the
gmon.out
file - View results with
gprof
$ g++ myProgram.cpp -pg
$ ./a.out
$ gprof ./a.out gmon.out # optionally add '| less' to view with pager
- A function will not appear in the output if it is never called or has been inlined with compiler optimisations
- There may be some inaccuracies if running times are short, e.g. %s over 100, this is due to approximations
C. Manual instrumentation the <chrono>
header enables “manual instrumentation” or timing in a program. Run the program/function multiple times and average to reduce noise
See Timer chapter
std::chrono::high_resolution_clock::now(); // get the current time, do before/after
17.2 General Optimisations
Datastructures and Algorithms
Use (correct) STL containers wherever possible
- Arrays/Vectors: fast random access
- Lists: fast insertion, slow traversal
- Trees: middle ground, fastest if both features are needed
Use (correct) library for an algorithm. Many available at Netlib.org
The main advantages of professional libraries:
- bug free, thoroughly tested
- optimised
- well documented (at least better than your code)
- support most architectures
Loop unrolling
//Dot product
double s = x[0] * y[0] + x[1] * y[1] + x[2] * y[2];
//instead of
for (int i=0; i<3; ++i)
s += x[i] * y[i];
Loop unrolling creates faster code for two main reasons:
- No control statements/branches in the loop
- Easier to pipeline
Partial loop unrolling is also possible:
for (int i=0; i<N; i+=4) {
a[i] = b[i] * c[i];
a[i+1] = b[i+1] * c[i+1];
a[i+2] = b[i+2] * c[i+2];
a[i+3] = b[i+3] * c[i+3];
}
Compiler Optimisations
See Week 11 Slides 19-33
Note: Functions may be inlined before the optimisations are done, e.g. getters
See 1.2 for compiler flags
Storage order and optimising cache
See Cache Chapter 17.
Multi dimensional arrays are generally stored linearly in memory. C/C++ uses row-major order (depicted),
Fortran, Matlab use column-major. Red box represents one cache line
For large matrices you can use block multiplication to insure that the calculations can remain in-cache
17.3 Template Meta Programming
Using templates to execute (parts of) a program or optimise it at compile time
Examples: calculating constants, loop unrolling, expression templates
Also see Wikipedia
I. Calculating Constants - Factorial
We can store the value in several ways: enums, static consts or with functions
Result will be fully calculated at compile time in all cases (some require more optimisation)
A. Result stored in the enum
(or static const
in comments)
template<int i>
struct Factor{
enum { Result = i * Factor<i-1>::Result };
//static const int Result = N * Factorial<N-1>::Result;
//static constexpr int Result = N * Factorial<N-1>::Result;
};
template <>
struct Factor<1>{
enum { Result = 1 };
//static const int Result = 1;
//static constexpr int Result = 1;
};
int main(int argc, char const *argv[]) {
std::cout << Factor<5>::Result; //retrieve value like this
}
B. Using static functions
in a struct, template number N
stored by struct
template<int N>
struct Factorial{
static int f(){
return N * Factorial<N-1>::f();
}
};
template<>
struct Factorial<0> { //specialised for N=0, use as a base case for the recursion
static int f() {
return 1;
}
};
int main() {
std::cout << Factorial<5>::f(); //can call directly as it's static
}
C. Using normal functions
template<int N>
int f(){
return N * f<N -1>();
}
template<>
int f<0>() {
return 1;
}
int main() {
std::cout << f<5>();
}
Note: It can be easy to confuse normal template parameters with meta-template parameters (N here)
E.g. when making factorial work for T
not just int
II. Loop Unrolling
Use meta-programming to unroll loops, optimising for short loops (removed loop overhead)
See Week 11a Slides 9-15
- Use
meta_dot
to recursively expand the cwise operation- Hence we need a base case $\implies$ template specialisation for
I = 0
- Hence we need a base case $\implies$ template specialisation for
- Uses a static function similar to C. above
- Generic for any size(
I
) and type(T
)
III. Expression Templates
Store an expression in an object and only evaluate at end, often used in Linear Algebra
Removes writing temp results, define how to best evaluate an expression at compile time $\implies$ faster
Essentially Templated Lazy Evaluation
Sidenote: Do not use the auto
keyword with expression templates, as it will take the type of the expression object
General Idea:
- Overload operators to return an expression object (store what needs to be evaluated)
- Say how to evaluate expression object best, either
operator=
in the vector classoperator[]
in the expression class, to evaluate just a single element
See non-templated Lazy Evaluation: Week 11a Slides 20-21
vectorsum
is the expression object
See proper Expression Template: Week 11 Demos etvector.hpp
X
is the expression object, is generic for any LHR, RHS and operator- Have a class per operator, stores how to apply the operation
operator+
at end sets up expression
18. BLAS & LAPACK
Fortran libraries are very fast and have been optimised over a long time. We can use them similar to a normal C++ library. BLAS is for basic linear algebra, LAPACK extends BLAS
Naming Convention:
Function names follow PTTXXX
P
- Precision of the data,D
=double,S
=Single,C
=Complex,Z
=Double ComplexTT
- Matrix Type,GE
=General,SY
=Symmetric,HE
=HermitianXXX
- Operation Name, e.g.EV
=Eigen Values
Note: Fortran is case insensitive
How to use:
- Declare function in C++, with correct naming and parameters, see docs(!)
- We only pass references or pointers of built-in data types, references by default
- Need to surround declaration with
extern “C”{...}
to prevent name mangling when compiling - Function names end with an underscore (except with certain compilers)
- Functions have a
void
return type, but return data through the referenced parameters, so we can return multiple variables, e.g. LU decomposition returns both matrices
Worth looking at what the inputs and outputs are in the docs
- Call function in C++ normally
- Link libraries when compiling
- Link library itself,
-llapack
or-lblas
- Link the Fortran compiler called gfortran with
-lgfortran
(not always required)
- Link library itself,
Example
- a) Find docs
- b) Declare the function in C++ by copying the docs
extern "C" { //Function ends in underscore
void dsyev_( const char& JOBZ //Reference by default
, const char& UPLO
, const int& N
, double* A //Pointer as signified by the (*) in the docs
, const int& LDA
, double* W
, double* WORK
, const int& LWORK
, int& INFO );
}
- Call the function
std::vector<double> M(N * N, 0); // pre-allocate the memory for the output in C++!
std::vector<double> omega(N);
std::vector<double> exact_omega(N);
double D_LWORK;
dsyev_('V', 'L', N, M.data(), N, omega.data(), &D_LWORK, -1, info);
//use vector.data() to get a pointer to the first element, as with an array
//as we need to pass by pointer
-
Link library in compilation, link
gfortran
last$ g++ main.cpp -llapack -lblas -lgfortran
OR with CMake
target_link_libraries(mytarget lapack blas gfortran) #link blas in the same way
OR link a pre-compiled library, e.g. on Mac we can use accelerate
find_library(ACCELERATE_LIB Accelerate ) mark_as_advanced(ACCELERATE_LIB) target_link_libraries(mytarget ${ACCELERATE_LIB})
19. Input / Output
19.1 Formatting Streams
Streams will first convert the value to a string then print/store it, we can control this conversion with <iomanip>
or <iostream>
Use these like: cout << std::setw(5)
similar to std::endl
setw(5)
sets width/characters to print for the next value, non-stickysetfill('0')
set fill character, so output matches defined widthleft
,right
,internal
sets the text alignment by setting position of fill characters to the oppositeinternal
is works for specific types, see example
scientific
prints floats in scientific notation, i.e. with exponentsetprecision(6)
changes the float precision, significant digits
Note: Functions are sticky unless specified, meaning they affect all following output, not just the next value
Examples
std::cout << std::setw(4) << std::setfill('0') << std::right << 5;
//output: 0005
std::cout << std::setw(4) << std::setfill('#') << std::left << 5;
//5###
//adding in std::
cout << setw(8) << setfill('@') << internal << setprecision(2) << -1.234;
//-@@@@1.2
for (int i = beg; i < end; ++i) { //Common in loop to print a table
cout << std::scientific;
cout << std::setw( 5) << i << " "
<< std::setw(15) << std::pow(M_PI, i) << " "
<< std::setw(15) << std::pow(M_PI,2*i) << std::endl;
}
Sidenote: Streams should be passed by a non-const reference, cannot be copied
19.2 File Streams
File IO in CLI
Programs can be executed in sequence in the terminal by redirecting output, called pipelining
$ a.exe | b.exe | c.exe
-b
has the input froma
etc…
We can redirect the output to a file, same for input
-
$ ./simulation.exe > data.txt
output to file -
$ ./a.out < input
input from file
You can redirect only one stream:
1>
stdout2>
stderr&>
error & output
File IO in C++
ofstream
behaves similar to std::cout
, same for ifstream
(‘of’ for ‘output file’ stream)
#include <iostream>
#include <fstream>
int main () {
std::ofstream outFile("output.txt"); //Open file with filestream
outFile << "Hello";
outFile.close(); //Close the file once finished(!)
int a;
std::ifstream inFile; //Same for ifsteams
inFile.open("input.txt");
inFile >> a;
inFile.close(); //Close also called in dtor at end of scope, best to close yourself
}
Can set different modes for an fstream
in the ctor/open:
fstream::in
input mode orout
, already set by default forifstream
orofstream
fstream::binary
binary modefstream::ate
output position starts at endfstream::app
append to end of filefstream::trunc
truncate, overwrite existing file (default mode)
std::ifstream in;
in.open ("input.txt", std::fstream::binary);
std::fstream out; //note only an fstream not ofstream, so need to set out mode
out.open ("output.txt", std::fstream::out | std::fstream::ate);
//input and output also possible
std::fstream file("out", std::ios::in | std::ios::out | std::ios::ate);
Note: The bitwise-or |
is used to set multiple options as a bitmask
HDF5
Common format for storing data for input/output, is quite complicated at first
- Use this for larger datasets
- Enables parallel I/O, compression, multiple files in Groups (file system)
20. Python
- Interpreted - when run, code is ‘compiled’ line by line directly from source code (
.py
)- Can run in a shell, e.g. ipython
- Untyped, Dynamic variables - type is not declared explicitly and the variables can change type
- Classes are not required to have all their variables defined initially
- Variables are references by default, almost all things in Python are objects, including functions
- Has a Garbage Collector, do not need to manage memory ourselves
- There is no privacy, just follows convention of underscores
var
public,_var
private,__var
strictly private
- Naming conventions follow PEP8, generally everything is lowercase, underscore separated except classes being CamelCase
- See the Python cheatsheet
20.1 Python Modules
Modules are roughly equivalent to libraries in C++. Any .py
file is a module, do not need to do anything special to use it as a module. We can #include
them by using import
import file # Imports whole file, similar to #include, then use file.function1()
import path.file # Can use a relative path, use . instead of /
import numpy as np # Can rename imported file/function/class
from file import function1, function2 # import only specific functions/classes
# Can call function1 directly like this -> may get name conflicts
Note: a package is a collection of modules, whereas a module is usually a single .py
Instead of a main()
in C++ we can have several ‘entry points’ in the project. The module which is called directly has its name set the main
if __name__ == "__main__": # equivalent to main() in C++
# This .py was executed directly, not imported
We can write the code without this, but then the code is executed when we import the .py
as a module
Common Packages:
- NumPy - Fast package for multi-dimensional arrays with linear algebra functions
- See the NumPy Cheat Sheet, SciPy extends NumPy
- Matplotlib - 2D plotting library
- H5py - Python interface for HDF5 file format
- Mpi4py - Message Passing Interface. Allows parallel programming with Python
- SymPy - Computer Algebra System, enables symbolic mathematics
20.2 Classes
The constructor is a magic method named __init__
- As we cannot overload functions in python, we create a single ctor with all possible parameters, then do a case distinction. Remember: arguments have default values and we can set them explicitly
class Genome(object):
def __init__(self, genome=None):
if genome is None:
self.genes = np.zeros(self.__class__.gene_size, dtype=bool)
else:
self.genes = copy.deepcopy(genome.genes)
# Can construct with either
a = Genome()
b = Genome(a)
c = Genome(genome=a) # setting args explicitly is more useful when there are more
Static variables
- Create by defining inside class, not in a function
- Inside class, access with
self.__class__.static_var
- Do not use
static_var
directly, it sees it as a local variable (so may get conflicts)
- Do not use
- Only use
MyClass.static_var
outside the class, as it can be overriden by an instance specific variable within the class
class Genome(object):
gene_size = 64 # static class variable, shared by all instances
def __init__(self):
self.genes = np.zeros(self.__class__.gene_size, dtype=bool)
# Access from outside class
Genome.gene_size = 128
Copying Instances
- As everything is a reference, if we assign a variable to another we are creating an equality
copy.copy(instance)
, does a shallow copy- First layer of variables is cloned, but these may be references to more objects
copy.deepcopy(instance)
, does a deep copy, usually what is wanted- This ‘clones’ everything recursively
import copy # required module for copying
x = [0,1,2]
y = x # makes x and y equivalent, y is a reference to x essentially
y[2] = 666
print(x) # [0, 1, 666] x is also changed
print(y) # [0, 1, 666]
x = [0,1,2]
y = copy.copy(x)
y[2] = 666
print(x) # [0, 1, 2] x unaffected
print(y) # [0, 1, 666]
x = [0,1,[2,3]] # Note 3rd elem of array is a *reference* now to another array
y = copy.copy(x)
y[2][0] = 666
y[0] = 333
print(x) # [0, 1, [666, 3]] x[0] unaffected but x[2] has changed
print(y) # [333, 1, [666, 3]]
x = [0,1,[2,3]]
y = copy.deepcopy(x)
y[2][0] = 666
print(x) # [0, 1, [2 , 3]]
print(y) # [0, 1, [666, 3]]
Inheritance
- Classes always inherit everything, as there is no privacy, everything is virtual
- We can call base methods with the
super()
- Only derived function is called by default, including in
__init__
- Only derived function is called by default, including in
- Functions are overriden if the names match
class A:
def __init__(self):
self.a = 1
def print_A(self):
print(self.a)
class B(A): # B inherits from A
def __init__(self):
super().__init__() # self.a == 1
self.b = 2
def print_B(self):
print(self.b)
b = B()
b.print_A() # 1, so print_A is inherited and base class constructor is called to set a
b.print_B() # 2
20.3 Decorators
Decorators are functions that modify the functionality of other functions. We apply these with the @decorator
in front of a function definition
Built-in Decorators
Static Functions
@staticmethod
- makes a function static, does not receive theself
@classmethod
- like static method but also has thecls
argumentcls
is the class type we call the function with- Useful with inheritance, as we can get the derived classes
Property
This is makes the function behave like a getter, but it allows us to use the function as if it were a variable, so don’t need to write the parentheses()
class Point:
...
@property # Like a getter, note: @x.getter does not exist
def x(self):
"""The point's x coordinate"""
return self._x
@x.setter # Setter for the property x, unrelated to _x
def x(self, val):
if val < 0: # e.g. Validate the new value in setter
val = 0
self._x = val
P1 = Point(1.,3.)
print("P1.x =", P1.x) # dont need to write brackets, i.e. P1.x(), use like a variable
P1.x = 2 # Set like a variable
- Define a function
myproperty
that returns a value, like a getter, then decorate with@property
- The corresponding setter is then
@myproperty.setter
@myproperty.getter
does not exist, it is fulfilled by the@property
- The corresponding setter is then
Custom Decorators
Here we define ourselves how a decorator modifies a function. The key is f = mydecorator(f)
def mydecorator(func):
def inner():
print('Hello', end=' ')
func()
return inner # when called we return the *function* inner as the modified function
@mydecorator # f = mydecorator(f)
def f():
print('World')
f() # Hello World