C++ Classes — Deep Dive Study Guide | cylinder2d.
cpp Page 1
C++ Classes
A Complete, Example-Driven Deep Dive
From first principles — objects, methods, constructors, access, inheritance, polymorphism
— to every class in [Link]
Logical · Engineering · Tree-Structure format
Based on the OpenLB [Link] example
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 2
Chapter 0
Learning Map — Your Path Through This
Guide
This guide is structured as a progressive journey. Each chapter adds one layer to your understanding of C++
classes. Read front-to-back the first time, then use the chapter list as a reference.
Chap Topic What you will be able to do after reading
ter
0 Learning map Navigate this guide
1 The problem classes solve Explain WHY classes exist
2 Class anatomy Name every part of a class declaration
3 Data members (fields) Declare and use instance variables
4 Member functions Write and call methods; understand const methods
(methods)
5 Access specifiers Use public, private, protected correctly
6 Constructors Read and write all forms of constructors
7 Destructors Understand automatic cleanup
8 The dot and arrow operators Access members of objects and pointers
9 Static members Understand class-wide shared data/functions
10 Inheritance Read base/derived class relationships
11 Virtual functions Understand polymorphism and override
12 const correctness Apply const to members and functions
13 Object lifetime & memory Know when objects are created/destroyed
14 Every class in cylinder2d Explain every OpenLB class in the file
15 Common mistakes Avoid the 10 most frequent class errors
■ Tip
Chapters 1–8 are the essential foundation. Read them carefully and try each code example
mentally. Chapters 9–13 add important details. Chapter 14 is the payoff — where everything
connects to [Link].
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 3
Chapter 1
The Problem Classes Solve — WHY Do They
Exist?
1.1 The world before classes — functions and loose data
Imagine you need to write a program to manage bank accounts. Without classes, you use separate variables
and separate functions:
// WITHOUT classes — data and functions are disconnected
double balance1 = 1000.0;
double balance2 = 500.0;
std::string owner1 = "Alice";
std::string owner2 = "Bob";
// Functions must receive data as parameters — fragile!
void deposit(double& balance, double amount) {
balance += amount;
}
bool withdraw(double& balance, double amount) {
if (amount > balance) return false;
balance -= amount;
return true;
}
// Usage — easy to accidentally mix up variables:
deposit(balance1, 200); // OK: Alice deposits
deposit(balance2, balance1); // BUG: passed wrong thing — no compiler help!
Without classes: data and logic are disconnected — error-prone
Problems with this approach:
• Data and the functions that operate on it are physically separated — nothing enforces they belong
together
• You can accidentally pass the wrong variable — the compiler cannot catch it
• With 100 accounts, you have 200 loose variables and no structure
• Adding a new field (e.g. account number) requires changing every function
1.2 Classes solve this — bundle data AND behaviour together
// WITH classes — data and behaviour are one unit
class BankAccount {
private:
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 4
double balance; // data hidden inside the class
std::string owner;
public:
// Constructor: sets up the account when created
BankAccount(std::string name, double initial)
: owner(name), balance(initial) {}
void deposit(double amount) { balance += amount; }
bool withdraw(double amount) {
if (amount > balance) return false;
balance -= amount;
return true;
}
double getBalance() const { return balance; }
std::string getOwner() const { return owner; }
};
// Usage — clean, safe, impossible to confuse accounts:
BankAccount alice("Alice", 1000.0);
BankAccount bob("Bob", 500.0);
[Link](200.0); // clearly Alice's account
[Link](100.0); // clearly Bob's account
[Link]([Link]()); // readable and intentional
With classes: data and logic are one unit — safe and readable
■ Analogy
A class is like a blueprint for a machine. The blueprint defines what the machine contains (its parts
= data members) and what it can do (its buttons = methods). An object is a specific machine built
from that blueprint. You can build many machines from one blueprint — each has its own parts but
shares the same instruction manual (methods).
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 5
Chapter 2
Class Anatomy — Every Part of a Class
Declaration
2.1 The visual map
← Class name declaration
class BankAccount {
private:
double balance;
std::string owner;
← Data members (fields)
void validate(double amt);
public: ← Private method
BankAccount(string name, double b);
void deposit(double amount);
← Constructor
bool withdraw(double amount);
double getBalance() const;
← Member functions (methods)
~BankAccount();
}; ← Closing brace + semicolon
Destructor
Figure 1 — Anatomy of a C++ class
2.2 The six parts of a class
Part Keyword / symbol Required? What it does
Class keyword class or struct YES Declares that what follows is a class
definition
Class name identifier YES The name you use to create objects of
this type
Opening brace { YES Starts the class body
Access specifier public / private / Recommende Controls who can see the members
protected d below it
Members variables & functions Usually The data and behaviour of the class
Closing brace + ; }; YES Ends the class definition. The
semicolon is REQUIRED
// Minimal legal class — technically valid but useless:
class Empty {};
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 6
// class vs struct: the ONLY difference is default access
class MyClass { /* default: private */ };
struct MyStruct { /* default: public */ };
// In OpenLB, 'struct' is often used for simple data-holder types
// and 'class' for full-featured simulation objects.
■ Watch out
The semicolon after the closing brace }; is unique to classes (and structs, enums). Functions do
NOT need a semicolon after }. Forgetting it causes confusing compile errors on the NEXT line.
2.3 class vs struct — the only real difference
class Dog { struct Dog {
// default: private // default: public
std::string name; std::string name; // accessible directly!
int age; int age;
}; };
Dog d1;
[Link] = "Rex"; // ERROR: name is private in class
Dog d2;
[Link] = "Rex"; // OK: name is public in struct
// In practice: use struct for simple data bags, class for full objects.
// OpenLB uses struct for descriptors (D2Q9) and class for simulation
objects.
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 7
Chapter 3
Data Members — The State of an Object
3.1 What are data members?
Data members (also called fields, instance variables, or attributes) are variables declared inside a class.
Each object gets its own separate copy of every data member:
class Particle {
public:
double x, y; // position
double vx, vy; // velocity
double mass; // mass
int id; // unique identifier
};
// Each object has its OWN copy of all fields:
Particle p1; p1.x = 1.0; p1.y = 2.0; [Link] = 1.5;
Particle p2; p2.x = 5.0; p2.y = 3.0; [Link] = 2.0;
// p1 and p2 are completely independent — changing p1.x does NOT affect p2.x
p1.x = 99.0;
// p2.x is still 5.0
Two BankAccount objects in memory:
obj1 obj2
balance 8 bytes balance 8 bytes
owner ptr 8 bytes owner ptr 8 bytes
Figure 4 — Data is per-object (separate copies); methods are shared (one copy in memory)
Methods (shared code)
deposit, withdraw, getBalance...
3.2 Static data members — shared by ALL objects
A static data member is shared across all instances of the class — there is only one copy no matter how
many objects you create:
class Particle {
public:
double x, y; // per-object: each particle has its own
static int particleCount; // shared: one value for ALL particles
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 8
Particle() { particleCount++; } // increment when created
~Particle(){ particleCount--; } // decrement when destroyed
};
// MUST define the static member outside the class:
int Particle::particleCount = 0;
Particle p1, p2, p3;
Particle::particleCount; // = 3 (shared by all)
[Link]; // also 3 — same value accessed via object
■ Tip
In OpenLB, static members are used in descriptor classes (like D2Q9) to store compile-time
constants: number of velocities, weight factors, lattice vectors — values that are the same for all
lattice cells and never change.
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 9
Chapter 4
Member Functions — The Behaviour of an
Object
4.1 What is a member function?
A member function (also called a method) is a function declared inside a class. It can access ALL of the
class's data members directly, without them being passed as parameters. This is the key power of classes —
the function 'knows' its data:
class Thermometer {
private:
double tempCelsius;
public:
Thermometer(double t) : tempCelsius(t) {}
// Method: accesses tempCelsius directly — no parameter needed
double getCelsius() const { return tempCelsius; }
double getFahrenheit() const { return tempCelsius * 9.0/5.0 + 32.0; }
double getKelvin() const { return tempCelsius + 273.15; }
void setTemp(double t) { tempCelsius = t; }
void heatBy(double dt) { tempCelsius += dt; }
};
Thermometer t(25.0); // 25°C
[Link](); // returns 77.0°F
[Link](10.0); // now 35°C
[Link](); // returns 308.15 K
4.2 const member functions — the 'I promise not to modify' declaration
A method marked const after its parameter list promises: "calling this function will NOT change any data
member." This is critically important in OpenLB where you pass objects as const& references:
class Circle {
double radius;
public:
Circle(double r) : radius(r) {}
double getRadius() const { return radius; } // const: safe read-only
double area() const { return 3.14159 * radius * radius; }
double perimeter() const { return 2 * 3.14159 * radius; }
void scale(double factor) { radius *= factor; } // non-const: modifies
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 10
};
// A const object can ONLY call const methods:
const Circle c(5.0);
[Link](); // OK: area() is const
[Link](2.0); // COMPILE ERROR: scale() is non-const on a const object
// A const reference can ONLY call const methods:
void printInfo(const Circle& c) {
[Link](); // OK
[Link](2.0); // COMPILE ERROR
}
■ In OpenLB
In [Link], the UnitConverter is passed as const UnitConverter<T,DESCRIPTOR>&
converter. This means only const methods of converter can be called inside that function.
getLatticeTime(), getCharLatticeVelocity(), getLatticeRelaxationFrequency() etc. are all const
methods — they only read, never modify.
4.3 The 'this' pointer — the hidden parameter
Every non-static member function receives a hidden pointer called this that points to the object it was called
on. You rarely need to use it explicitly, but understanding it helps:
class Counter {
int count;
public:
Counter(int c) : count(c) {}
void increment() {
count++; // same as: this->count++
// 'this' is a hidden pointer to the current object
}
// Explicit 'this' use — chaining:
Counter& add(int n) {
this->count += n;
return *this; // return the object itself for chaining
}
};
Counter c(0);
[Link](5).add(3).add(2); // chaining: [Link] = 10
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 11
■ Tip
You see method chaining in OpenLB with the output stream: clout << "x=" << x << std::endl; Each
<< returns the stream itself (*this), enabling the next <<.
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 12
Chapter 5
Access Specifiers — public, private,
protected
5.1 The three access levels — a visual map
public
— visible EVERYWHERE: inside the class, from objects, from derived classes, from anywhere
protected
— visible INSIDE the class and in DERIVED classes only
private
— visible ONLY inside THIS class (default for 'class' keyword)
Figure 3 — Access specifier zones (nested: outer = widest access)
5.2 private — the default for class, the most restrictive
Private members are accessible only from within the class itself. Not from derived classes. Not from user
code. Only from the class's own methods:
class Password {
private:
std::string secret; // only Password's own methods can touch this
public:
Password(std::string s) : secret(s) {}
bool check(std::string attempt) { return attempt == secret; }
// Notice: no 'getSecret()' method — intentional!
};
Password p("hunter2");
[Link]; // COMPILE ERROR: secret is private
[Link]("hunter2"); // OK: check() is public and internally accesses secret
■ Key concept
Encapsulation: hiding private data and exposing only a controlled public interface is one of the most
important principles of object-oriented design. It lets you change the internal implementation without
breaking user code.
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 13
5.3 public — accessible from everywhere
class Sensor {
public: // everything below is public
double read() { return rawValue * calibration; }
void calibrate(double c) { calibration = c; }
private:
double rawValue = 0.0; // hidden
double calibration = 1.0; // hidden
};
Sensor s;
[Link](1.05); // OK: public method
[Link](); // OK: public method
[Link]; // ERROR: private
5.4 protected — for inheritance
Protected members are private to outside code, but accessible to derived classes (children in inheritance).
You will see this in OpenLB's base classes:
class Animal {
protected:
double energy; // derived classes can access this
std::string name;
public:
Animal(std::string n, double e) : name(n), energy(e) {}
std::string getName() const { return name; }
};
class Dog : public Animal {
public:
Dog(std::string n) : Animal(n, 100.0) {}
void eat(double food) {
energy += food; // OK: energy is protected — Dog CAN access it
}
void bark() {
energy -= 5.0; // OK: same
}
};
Dog rex("Rex");
[Link] = 200; // COMPILE ERROR: protected — outside code cannot touch it
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 14
[Link](10); // OK: public method
■ In OpenLB
In OpenLB's IndicatorF2D base class, the bounding-box arrays _myMin and _myMax are protected
— derived classes like IndicatorCircle2D can read and set them, but user code (your
[Link]) cannot access them directly.
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 15
Chapter 6
Constructors — Building an Object
6.1 What is a constructor?
A constructor is a special member function that is called automatically when an object is created. Its job is
to put the object into a valid initial state. Two rules make it special: it has the same name as the class and it
has no return type (not even void):
class Robot {
private:
std::string name;
int batteryLevel;
bool isOn;
public:
// Constructor — same name as class, no return type
Robot(std::string robotName, int battery) {
name = robotName;
batteryLevel = battery;
isOn = false; // always starts off
}
void turnOn() { isOn = true; }
bool alive() const { return batteryLevel > 0; }
};
// Constructor called automatically when object is created:
Robot r1("R2D2", 100); // [Link]="R2D2", [Link]=100, [Link]=false
Robot r2("C3PO", 80); // r2 is independent — its own copy of all fields
6.2 The member initialiser list — the preferred syntax
The preferred way to initialise members in a constructor is using the member initialiser list — the colon
syntax between the parameter list and the body. This initialises members directly instead of assigning to
them after construction (more efficient and required for const members and references):
class Robot {
std::string name;
int batteryLevel;
const int serialNumber; // const member — MUST use initialiser list
public:
// Initialiser list style (preferred):
// : member1(arg1), member2(arg2), ...
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 16
Robot(std::string n, int bat, int serial)
: name(n), batteryLevel(bat), serialNumber(serial) {
// body: for additional logic only
}
// Assignment style (works but less efficient for objects):
Robot(std::string n, int bat) {
name = n; // first default-constructs name, then assigns
batteryLevel = bat; // inefficient for std::string
// serialNumber = 5; // COMPILE ERROR: can't assign to const after
construction
}
};
■ In OpenLB
Every OpenLB constructor uses the initialiser list style. Example from UnitConverter: the six
physical parameters (physDeltaX, physDeltaT, charPhysLength, etc.) are all stored as const
members and must be initialised via the list, not assigned in the body.
6.3 Types of constructors
Type Syntax Called when Example
Default constructor ClassName() Object created with no Robot r;
arguments
Parameterised ctor ClassName(args) Object created with Robot r("R2D2", 100)
arguments
Copy constructor ClassName(const C& Object copied from another Robot r2 = r1;
other)
Move constructor ClassName(C&& other) Object moved (transfer Robot r2 =
ownership) std::move(r1)
Delegating ctor ClassName() : One ctor calls another ctor Robot() :
ClassName(args) Robot("default",50){}
class Point {
double x, y;
public:
Point() : x(0), y(0) {} // default constructor
Point(double a, double b) : x(a), y(b) {} // parameterised
Point(const Point& p): x(p.x),y(p.y) {} // copy constructor
};
Point p1; // default ctor: p1 = (0, 0)
Point p2(3.0, 4.0); // parameterised: p2 = (3, 4)
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 17
Point p3 = p2; // copy ctor: p3 = (3, 4), independent copy
Point p4(p2); // same as above — explicit copy constructor call
6.4 Constructor delegation — one constructor calling another
class Simulation {
int nx, ny;
double physLength;
double viscosity;
public:
// Full constructor:
Simulation(int x, int y, double L, double nu)
: nx(x), ny(y), physLength(L), viscosity(nu) {}
// Delegating constructor — calls the full one with defaults:
Simulation() : Simulation(100, 50, 2.2, 0.001) {}
// Another delegating constructor:
Simulation(int resolution) : Simulation(resolution, resolution/2, 2.2,
0.001) {}
};
Simulation s1; // default: 100x50, L=2.2, nu=0.001
Simulation s2(200); // 200x100
Simulation s3(220, 110, 4.4, 0.002); // full specification
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 18
Chapter 7
Destructors — Automatic Cleanup
7.1 What is a destructor?
A destructor is called automatically when an object's lifetime ends — when it goes out of scope or is deleted.
It has the same name as the class but prefixed with a tilde ~ and takes no parameters:
class FileWriter {
FILE* file;
public:
FileWriter(const char* path) {
file = fopen(path, "w");
std::cout << "File opened" << std::endl;
}
void write(const char* text) { fputs(text, file); }
~FileWriter() { // destructor: ~ prefix, no parameters
fclose(file); // automatically close file
std::cout << "File closed" << std::endl;
}
};
void doWork() {
FileWriter fw("[Link]"); // constructor called: file opened
[Link]("hello\n");
[Link]("world\n");
// Function ends -> fw goes out of scope -> destructor called: file
closed
} // <- destructor runs HERE automatically
■ Key concept
This automatic cleanup pattern is called RAII: Resource Acquisition Is Initialisation. The resource
(file, memory, GPU buffer) is acquired in the constructor and released in the destructor. You never
need to manually call cleanup code. OpenLB uses this extensively — SuperLattice allocates GPU
memory in its constructor and frees it in its destructor, automatically, when main() exits.
7.2 Destructor call order
int main() {
// Objects created in this order:
UnitConverter<T,D> converter(...); // 1st created
SuperGeometry<T,2> superGeometry(...); // 2nd created
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 19
SuperLattice<T,D> sLattice(...); // 3rd created
// ... simulation runs ...
return 0;
// Destructors called in REVERSE order:
// 1st destroyed: sLattice (frees GPU memory / lattice data)
// 2nd destroyed: superGeometry (frees geometry data)
// 3rd destroyed: converter (nothing to free — only numbers)
}
■ Tip
C++ always destroys local objects in reverse order of creation (Last In First Out). This is important
because sLattice may hold references to superGeometry — destroying sLattice first ensures it can
clean up safely before superGeometry disappears.
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 20
Chapter 8
The Dot (.) and Arrow (->) Operators
8.1 Visual comparison
Dot operator . (object) Arrow operator -> (pointer)
SuperGeometry<T,2> sg; SuperGeometry<T,2>* ptr;
[Link](0, 2); ptr->rename(0, 2);
[Link](); ptr->clean();
[Link](); // same as (*ptr).clean()
sg is an OBJECT (not a pointer) ptr is a POINTER to an object
Figure 6 — Dot (.) vs Arrow (->) operator: object vs pointer access
8.2 Dot operator — for objects
// Dot operator: [Link] or [Link](args)
SuperGeometry<T,2> superGeometry(...); // superGeometry IS an object
[Link](0, 2); // call method rename()
[Link](); // call method clean()
[Link](); // call method checkForErrors()
[Link](); // call method print()
// More examples with other types:
std::string name = "Alice";
[Link](); // call length() method on the string object
[Link](0, 3); // call substr() method
UnitConverter<T,D> converter(...);
[Link](maxPhysT); // call method on converter
[Link]();
[Link]();
8.3 Arrow operator — for pointers to objects
// Arrow operator: ptr->member or ptr->method(args)
// ptr->method() is exactly the same as (*ptr).method()
SuperGeometry<T,2>* ptr = &superGeometry; // ptr is a POINTER
ptr->rename(0, 2); // same as (*ptr).rename(0, 2)
ptr->clean(); // same as (*ptr).clean()
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 21
// When do you see -> in OpenLB?
// Internally the library uses pointers and smart pointers.
// As a user, you mostly use objects directly (dot operator).
// However, singleton::mpi() returns a reference/object-like thing:
singleton::mpi().getSize(); // dot operator on the returned object
■ Tip
Rule: if the variable is declared as 'TypeName obj' use dot (.). If it is declared as 'TypeName* ptr'
use arrow (->). If you ever see 'invalid use of incomplete type' or 'request for member X in Y which
is of pointer type' — you are probably using the wrong operator.
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 22
Chapter 9
Static Members — Class-Wide Shared Data
and Functions
9.1 Static member functions — callable without an object
A static member function belongs to the class itself, not to any specific object. You call it with the class
name and ::, not via an object:
class MathUtils {
public:
// Static methods: no 'this' pointer, no object needed
static double clamp(double v, double lo, double hi) {
return (v < lo) ? lo : (v > hi) ? hi : v;
}
static double lerp(double a, double b, double t) {
return a + t * (b - a);
}
static constexpr double PI = 3.14159265358979;
};
// Call via class name — no object needed:
double clamped = MathUtils::clamp(7.5, 0.0, 5.0); // 5.0
double interp = MathUtils::lerp(0.0, 10.0, 0.3); // 3.0
double pi = MathUtils::PI;
// Can also call via an object (but unusual style):
MathUtils m;
[Link](7.5, 0.0, 5.0); // works but misleading
■ In OpenLB
singleton::mpi() in [Link] is a static-like pattern — it returns the one shared MPI
environment object. descriptor structs like D2Q9 have static constexpr members: the number of
velocities (q=9), lattice velocities c[], weights w[] — all shared across all uses of D2Q9.
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 23
Chapter 10
Inheritance — Building Hierarchies with 'is-a'
10.1 The concept
Inheritance lets a new class (derived class / child) automatically gain all members of an existing class (base
class / parent), then add its own members or override existing behaviour. The relationship is 'is-a':
// Base class: the general concept
class Shape {
protected:
std::string colour;
public:
Shape(std::string c) : colour(c) {}
std::string getColour() const { return colour; }
virtual double area() const = 0; // pure virtual: MUST override
virtual double perimeter() const = 0; // pure virtual
virtual ~Shape() {} // virtual destructor (important!)
};
// Derived class: inherits from Shape using ': public Shape'
class Circle : public Shape {
double radius;
public:
Circle(double r, std::string c) : Shape(c), radius(r) {}
double area() const override { return 3.14159 * radius * radius; }
double perimeter() const override { return 2 * 3.14159 * radius; }
double getRadius() const { return radius; }
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double w, double h, std::string c) : Shape(c), w(w), h(h) {}
double area() const override { return w * h; }
double perimeter() const override { return 2*(w+h); }
};
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 24
IndicatorF2D<T> (Base)
+ operator()(x,y) = 0
# _myMin[2]
# _myMax[2]
inherits from (is-a relationship)
IndicatorCuboid2D IndicatorCircle2DIndicatorCylinder3D
+ operator()(x,y) + operator()(x,y) + operator()(x,y,z)
- _extend[2] - _centre[2] - _centre[3]
- _origin[2] - _radius - _radius
Rect region Circle region 3D cylinder
Figure 5 — IndicatorF2D<T> inheritance hierarchy in OpenLB
10.2 Inheritance syntax breakdown
// Syntax: class DerivedName : AccessSpecifier BaseClassName { ... };
// ^^^^^^^^^^^^^^
// almost always 'public' for inheritance
class Circle : public Shape { ... };
// Circle IS-A Shape: a Circle object IS a Shape object
// Circle HAS everything Shape has, PLUS its own radius
// In OpenLB:
class IndicatorCircle2D : public IndicatorF2D<T> { ... };
// IndicatorCircle2D IS-A IndicatorF2D
// It has everything IndicatorF2D has, plus centre and radius
10.3 What is inherited and what is not
From base class Inherited by Notes
derived?
public members YES Accessible from derived and outside
protected members YES Accessible from derived only
private members NO (but they exist!) Data exists in memory but cannot be accessed
directly — use protected or accessor methods
Constructors NO Each class defines its own constructors
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 25
Destructor NO (but called!) Base destructor is called automatically after
derived destructor
Assignment operator NO (default Compiler creates one if not defined
synthesised)
class Dog : public Animal {
public:
Dog(std::string name) : Animal(name, 100.0) {
// Dog's constructor MUST call Animal's constructor
// to initialise the inherited parts
}
~Dog() {
// Dog's destructor runs first, then Animal's destructor
// automatically — no need to call it manually
}
};
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 26
Chapter 11
Virtual Functions and Polymorphism
11.1 The problem — without virtual
class Animal {
public:
void speak() { std::cout << "..." << std::endl; } // NOT virtual
};
class Dog : public Animal {
public:
void speak() { std::cout << "Woof!" << std::endl; }
};
// Without virtual — which speak() is called depends on the POINTER type:
Dog d;
[Link](); // 'Woof!' — obvious
Animal* ptr = &d; // Animal pointer pointing to a Dog
ptr->speak(); // '...' — WRONG! Called Animal's version
// because ptr is Animal* at compile time
Without virtual: the pointer type determines which method runs — bad!
11.2 virtual — runtime dispatch
class Animal {
public:
virtual void speak() { std::cout << "..." << std::endl; }
virtual ~Animal() {} // ALWAYS make destructor virtual in base classes
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Woof!" << std::endl; }
};
class Cat : public Animal {
public:
void speak() override { std::cout << "Meow!" << std::endl; }
};
// With virtual — the OBJECT type determines which method runs:
Animal* animals[3];
animals[0] = new Dog();
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 27
animals[1] = new Cat();
animals[2] = new Dog();
for (int i = 0; i < 3; i++) {
animals[i]->speak(); // 'Woof!', 'Meow!', 'Woof!'
} // correct! runtime dispatch based on actual type
With virtual: the actual object type determines which method runs — correct!
11.3 Pure virtual functions — abstract classes
// = 0 makes a function PURE VIRTUAL
// A class with any pure virtual function is ABSTRACT:
// - You cannot create objects of it directly
// - Derived classes MUST override it
class IndicatorF2D {
public:
// Pure virtual: every derived class MUST implement this
virtual bool operator()(bool output[], const double input[]) = 0;
virtual ~IndicatorF2D() {}
};
// IndicatorF2D obj; // COMPILE ERROR: cannot instantiate abstract class
// IndicatorCircle2D provides the implementation:
class IndicatorCircle2D : public IndicatorF2D<T> {
public:
bool operator()(bool output[], const double input[]) override {
double dx = input[0] - cx;
double dy = input[1] - cy;
output[0] = (dx*dx + dy*dy <= radius*radius);
return output[0];
}
};
IndicatorCircle2D c(center, radius); // OK: concrete class
// Pass as base class reference — polymorphism at work:
void useIndicator(IndicatorF2D<T>& ind) { /* calls correct operator() */ }
useIndicator(c); // works!
■ In OpenLB
Every IndicatorF2D-derived class in OpenLB overrides operator() to answer 'is point (x,y) inside this
region?'. This is exactly how prepareGeometry uses the circle indicator — it receives
IndicatorF2D<T>& and calls it as a function to test whether each cell is inside the cylinder.
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 28
Chapter 12
const Correctness — The Discipline of Not
Modifying What You Should Not
12.1 The four positions of const in class code
// POSITION 1: const data member — never changes after construction
class Converter {
const double physDeltaX; // set in constructor, never changes
const double physDeltaT;
public:
Converter(double dx, double dt) : physDeltaX(dx), physDeltaT(dt) {}
};
// POSITION 2: const method — does not modify the object
class Circle {
double r;
public:
double area() const { return 3.14159 * r * r; } // const method
void resize(double f) { r *= f; } // non-const method
};
// POSITION 3: const parameter — does not modify the argument
void printCircle(const Circle& c) { // c cannot be modified
[Link](); // OK: const method on const ref
[Link](2); // ERROR: non-const method on const ref
}
// POSITION 4: const return value — caller cannot modify the returned value
const double& getPi() { static const double pi=3.14159; return pi; }
getPi() = 3.0; // COMPILE ERROR: returned reference is const
■ Tip
A general rule: if a method does not change any data member, always mark it const. If a function
parameter is a class object you only need to read, always pass it as const&. This prevents bugs
and makes code self-documenting.
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 29
Chapter 13
Object Lifetime and Memory — Where Do
Objects Live?
13.1 Stack objects — automatic lifetime
void foo() {
// Stack objects — created automatically, destroyed automatically:
int x = 5; // primitive on stack
std::string s = "hello"; // object on stack
UnitConverter<T,D> conv(...); // OpenLB object on stack
// ... use x, s, conv ...
} // <- ALL of x, s, conv destroyed HERE automatically (RAII)
// In [Link] main():
int main() {
const UnitConverter<T,D> converter(...); // stack — auto destroyed
SuperGeometry<T,2> superGeometry(...); // stack — auto destroyed
SuperLattice<T,D> sLattice(...); // stack — auto destroyed
// ...
return 0; // all three destructors run here, reverse order
}
13.2 Heap objects — manual lifetime (new/delete)
// Heap objects — you control lifetime with new/delete:
BankAccount* acc = new BankAccount("Alice", 1000);
// acc is a POINTER. The object lives on the HEAP.
// It will NOT be destroyed when 'acc' goes out of scope!
acc->deposit(200); // use arrow operator for pointer
delete acc; // YOU must call delete — otherwise MEMORY LEAK
acc = nullptr; // good practice: null the pointer after delete
// Modern C++ uses smart pointers to avoid manual delete:
#include <memory>
auto acc2 = std::make_unique<BankAccount>("Bob", 500);
acc2->deposit(100); // use normally
// No delete needed! unique_ptr destructor calls delete automatically
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 30
■ Tip
In [Link], you never see raw new/delete — OpenLB manages its own memory internally
using smart pointers and RAII. You create objects on the stack and the library handles the rest.
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 31
Chapter 14
Every Class in [Link] — The
Complete Analysis
This chapter explains every class used in [Link]. For each class: what kind of class it is, what
data it holds, what methods it provides, and exactly what role it plays in the simulation.
14.1 UnitConverter<T, DESCRIPTOR>
const UnitConverter<T,DESCRIPTOR> converter(
(T) L, // physDeltaX = 0.01 m
(T) CFL*L/0.2, // physDeltaT = 0.0025 s
(T) 2.0*radiusCylinder, // charPhysLength = 0.1 m (diameter)
(T) 0.2, // charPhysVelocity = 0.2 m/s
(T) 0.2*2.*radiusCylinder/Re, // physViscosity = 0.001 m^2/s
(T) 1.0 // physDensity = 1 kg/m^3
);
[Link](); // logs all derived quantities
Property Detail
Kind Template class (class template)
Data members 6 const T values: physDeltaX, physDeltaT, charPhysLength, charPhysVelocity,
physViscosity, physDensity
Key methods getLatticeTime(), getPhysTime(), getCharLatticeVelocity(),
getLatticeRelaxationFrequency(), print()
All methods const — it is declared const, so no modification is allowed
Purpose Converts between SI units and LBM lattice units. Central reference for all unit
conversions.
Constructed Once, in main(). Passed by const& everywhere — efficient and safe.
14.2 SuperGeometry<T, 2>
SuperGeometry<T,2> superGeometry(cuboidDecomposition, loadBalancer);
prepareGeometry(converter, superGeometry, circle);
// prepareGeometry calls:
// [Link](0, 2);
// [Link](2, 1, {1,1});
// [Link](2, 3, 1, inflow);
// [Link](2, 4, 1, outflow);
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 32
// [Link](1, 5, circle);
// [Link]();
// [Link]();
// [Link]();
Property Detail
Kind Template class with type param T and non-type param 2 (dimensions)
Data members A distributed array of integers (the material numbers), one per lattice cell. For
220x42 grid = 9240 cells.
Key methods rename() — change material tags; clean() — remove isolated cells;
checkForErrors() — validate; print() — statistics
Purpose The geometry description layer. Tells the lattice WHERE walls are, where
inlet/outlet are, where the cylinder is.
Constructed In main() after CuboidDecomposition and LoadBalancer. Modified by
prepareGeometry().
Destroyed Automatically at end of main(). Destructor frees distributed geometry data.
14.3 SuperLattice<T, DESCRIPTOR>
SuperLattice<T,DESCRIPTOR> sLattice(superGeometry);
prepareLattice(sLattice, converter, superGeometry, circle);
// prepareLattice calls:
// [Link]<BGKdynamics>(superGeometry, 1);
// [Link](superGeometry, 1, rhoF, uF);
// [Link](superGeometry, 1, rhoF, uF);
// [Link]<OMEGA>(omega);
// [Link]();
// Main loop calls:
// [Link]();
// [Link](...);
Property Detail
Kind Template class: T=precision, DESCRIPTOR=lattice type (determines how many f
values per cell)
Data members For D2Q9: 9 doubles per cell x 9240 cells = ~83,000 doubles (~660 KB). Plus
boundary data, dynamics pointers.
Key methods collideAndStream() — the main LBM step; defineDynamics() — assign collision
rules; defineRhoU() / iniEquilibrium() — set initial state; setParameter() — set
omega
Purpose The heart of the simulation. Stores all distribution functions f_i. Executes the LBM
algorithm.
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 33
Constructed In main() taking superGeometry to know which cells exist.
GPU support setProcessingContext() moves data between CPU RAM and GPU VRAM.
14.4 IndicatorF2D<T>, IndicatorCuboid2D<T>, IndicatorCircle2D<T>
// In main():
Vector<T,2> center{centerCylinderX, centerCylinderY};
IndicatorCircle2D<T> circle(center, radiusCylinder);
// In prepareGeometry() body:
IndicatorCuboid2D<T> inflow(extend, origin); // inlet strip rectangle
IndicatorCuboid2D<T> outflow(extend, origin); // outlet strip rectangle
// In prepareGeometry() signature:
void prepareGeometry(..., IndicatorF2D<T>& circle) { // base class ref
[Link](1, 5, circle); // uses circle as a test function
}
Class Kind Key method Data held Purpose
IndicatorF2D<T> Abstract base operator()(point) Bounding box Interface: is point
class =0 inside?
IndicatorCuboid2D< Concrete derived operator()(point) origin[2], Rectangle region test
T> extend[2]
IndicatorCircle2D<T> Concrete derived operator()(point) centre[2], radius Circle region test
14.5 CuboidDecomposition2D<T> and HeuristicLoadBalancer<T>
// Decompose the full domain into sub-domains for parallel processing:
IndicatorCuboid2D<T> cuboid(extend, origin); // the full domain rectangle
CuboidDecomposition2D<T> cuboidDecomposition(
cuboid, // what to decompose
L, // minimum cuboid size
singleton::mpi().getSize() // how many pieces
);
// Assign pieces to MPI processes:
HeuristicLoadBalancer<T> loadBalancer(cuboidDecomposition);
// Uses a heuristic (greedy) algorithm to give each process equal work
Class Data held Purpose
CuboidDecomposition2D<T List of sub-rectangle boundaries Splits the 2.2x0.42m domain into N
> and sizes sub-domains, one per MPI rank
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 34
HeuristicLoadBalancer<T> Map: cuboid index → MPI rank Assigns cuboids to MPI processes
trying to balance work equally
14.6 AnalyticalConst2D<T,T> and Poiseuille2D<T> (Functors)
// Functor classes — objects that behave like functions
AnalyticalConst2D<T,T> rhoF(1); // returns 1.0 at every (x,y)
AnalyticalConst2D<T,T> uF(0, 0); // returns (0,0) at every (x,y)
// These ARE objects of a class. But they define operator():
// double density = rhoF(x, y); // calls operator() — returns 1.0
// Used to set initial conditions:
[Link](superGeometry, 1, rhoF, uF);
[Link](superGeometry, 1, rhoF, uF);
// Poiseuille2D: parabolic inlet profile functor
Poiseuille2D<T> poiseuilleU(superGeometry, 3, maxVelocity, distance2Wall);
// poiseuilleU(x, y) returns the correct velocity at point (x,y) on the inlet
[Link](superGeometry, 3, poiseuilleU);
Class Inherited from operator()(x,y) returns Purpose
AnalyticalConst2D<T,T> AnalyticalF2D (functor Constant value set at Uniform initial
base) construction conditions
Poiseuille2D<T> AnalyticalF2D (functor Parabolic velocity for point Inlet velocity
base) (x,y) on inlet profile
PolynomialStartScale<T, StartScale base Ramp fraction [0..1] at time t Smooth start-up
T> ramp
AnalyticalFfromSuperF2 AnalyticalF2D Interpolated field value at Query discrete
D<T> (x,y) fields at any point
14.7 SuperLatticePhysVelocity2D and SuperLatticePhysPressure2D
SuperLatticePhysVelocity2D<T,DESCRIPTOR> velocity(sLattice, converter);
SuperLatticePhysPressure2D<T,DESCRIPTOR> pressure(sLattice, converter);
[Link](velocity); // add to output writer
[Link](pressure);
// These are also FUNCTOR classes (inherit from SuperLatticeF2D)
// When evaluated at a cell, they convert raw f-populations
// to physical velocity [m/s] or pressure [Pa] using converter
// Used BOTH for file output AND for the pressure-drop measurement:
AnalyticalFfromSuperF2D<T> interpolatePressure(pressure, true);
T p1; interpolatePressure(&p1, point1); // pressure at upstream point
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 35
T p2; interpolatePressure(&p2, point2); // pressure at downstream point
Property SuperLatticePhysVelocity2D SuperLatticePhysPressure2D
Inherits from SuperLatticeF2D<T,D> SuperLatticeF2D<T,D>
References sLattice, converter sLattice, converter
Returns per cell 2D velocity vector [m/s] Scalar pressure [Pa]
Conversion u_lbm * (dx/dt) (rho_lbm - 1) * cs^2 * rho_phys *
dx^2/dt^2
Used for VTK output + visualisation VTK output + pressure-drop
measurement
14.8 SuperVTMwriter2D<T> and util::Timer<T>
// Output writer:
SuperVTMwriter2D<T> vtmWriter("cylinder2d");
[Link](); // create index file on first step
[Link](velocity); // register what to output
[Link](pressure);
[Link](iT); // write files for this time step
// Timer:
util::Timer<T> timer(iTmax, [Link]().getNvoxel());
[Link]();
// ... simulation loop ...
[Link](iT); // record current progress
[Link](); // print elapsed time, MLUPs (million lattice updates/s)
[Link]();
[Link](); // final performance report
Class Data members Purpose
SuperVTMwriter2D<T> File prefix, list of registered functors Writes VTK Multi-block files readable
by ParaView
util::Timer<T> Start time, iTmax, cell count, Measures wall-clock time, computes
history MLUPs performance metric
14.9 OstreamManager — the console output class
OstreamManager clout(std::cout, "getResults");
clout << "pressure1=" << p1;
clout << "; pressureDrop=" << pressureDrop << std::endl;
// Output: [getResults] pressure1=0.00312; pressureDrop=0.00205
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 36
// OstreamManager IS-A std::ostream (by inheritance)
// It wraps std::cout and prepends a prefix to each line
// In parallel (MPI) runs, it also prepends the process rank
// so you know which MPI process printed each line
// The << operator is overloaded in std::ostream and inherited:
// ostream& operator<<(int x);
// ostream& operator<<(double x);
// ostream& operator<<(const char* s);
// ... etc
14.10 Vector<T, 2> — the simple coordinate class
// In prepareGeometry and main():
Vector<T,2> extend(lengthX, lengthY); // size of domain
Vector<T,2> origin; // bottom-left corner
Vector<T,2> center{centerCylinderX, centerCylinderY};
// Index access with []:
extend[0] = 2.*L; // set X component
origin[0] = -L; // set X component of origin
// Vector<T,2> is a simple class holding exactly 2 values of type T.
// It overloads operator[] for element access.
// It is similar to std::array<T,2> but with OpenLB-specific methods.
14.11 BGKdynamics, BounceBack, InterpolatedVelocity,
InterpolatedPressure — Policy Classes
These are policy classes — they do not create objects that exist at runtime. Instead, they are passed as
template arguments to tell defineDynamics or boundary::set which algorithm to use. The compiler
generates different code for each policy:
// These are classes used AS TYPES (template arguments), not as objects:
[Link]<BGKdynamics>(superGeometry, 1);
// BGKdynamics: a class encoding the BGK collision algorithm.
// The template instantiation reads BGKdynamics::collide() and
// generates optimised code for that specific collision rule.
boundary::set<boundary::BounceBack>(sLattice, superGeometry, 2);
// BounceBack: a class encoding the reflection boundary rule.
// Its static constexpr members describe how populations are reflected.
boundary::set<boundary::InterpolatedVelocity>(sLattice, superGeometry, 3);
// InterpolatedVelocity: 2nd-order velocity boundary condition class.
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 37
boundary::set<boundary::InterpolatedPressure>(sLattice, superGeometry, 4);
// InterpolatedPressure: fixed-pressure (Dirichlet) boundary condition
class.
Policy class Algorithm Applied to material Physics
BGKdynamics BGK (Bhatnagar-Gross- 1 (fluid) f_i → f_i + omega*(f_eq
Krook) collision - f_i)
BounceBack Reflect populations back 2 (walls) No-slip: u_wall = 0
InterpolatedVelocity 2nd-order velocity BC 3 (inlet) Fixed velocity: Poiseuille
profile
InterpolatedPressure 2nd-order pressure BC 4 (outlet) Fixed pressure: p = 0
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 38
Chapter 15
The 10 Most Common Class Mistakes — and
How to Avoid Them
// MISTAKE 1: Missing semicolon after closing brace
class Dog { } // COMPILE ERROR: expected ';' after class definition
class Dog { }; // CORRECT
// MISTAKE 2: Calling a non-const method on a const reference
void info(const SuperGeometry<T,2>& sg) {
[Link](0, 1); // ERROR: rename() is non-const on const reference
[Link](); // OK if print() is a const method
}
// MISTAKE 3: Accessing private members from outside
class Circle { double radius; };
Circle c; [Link] = 5.0; // ERROR: radius is private (class default)
// MISTAKE 4: Forgetting to call the base constructor
class Dog : public Animal {
Dog() {} // ERROR if Animal has no default constructor
Dog() : Animal("Rex", 100) {} // CORRECT: call base ctor
};
// MISTAKE 5: Object slicing
Circle c(5.0, "red");
Shape s = c; // SLICES: copies only the Shape part, loses Circle data
Shape& r = c; // CORRECT: reference preserves the Circle
// MISTAKE 6: Forgetting virtual destructor in base class
class Base { ~Base(){} }; // non-virtual destructor
class Derived : public Base {};
Base* ptr = new Derived();
delete ptr; // Only calls ~Base() — ~Derived() never runs! Memory leak.
class Base { virtual ~Base(){} }; // CORRECT: virtual destructor
// MISTAKE 7: Using 'this' in constructor initialiser list order
class Bad {
int b;
int a; // declared AFTER b
public:
Bad(int x) : a(x), b(a) {} // UNDEFINED: a is initialised after b!
}; // Members initialise in DECLARATION order, not initialiser list order
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 39
// MISTAKE 8: const member without initialiser list
class Oops {
const int n;
public:
Oops(int x) { n = x; } // ERROR: const member must be in init list
Oops(int x) : n(x) {} // CORRECT
};
// MISTAKE 9: Multiple inheritance diamond problem (rare but confusing)
class A {};
class B : public A {};
class C : public A {};
class D : public B, public C {}; // D has TWO copies of A — ambiguous!
// Solution: virtual inheritance (advanced — not in [Link])
// MISTAKE 10: Infinite recursion in constructor delegation
class Bad2 {
Bad2() : Bad2(0) {} // calls parameterised ctor
Bad2(int n) : Bad2() {}// calls default ctor -> infinite loop!
};
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 40
Chapter 16
Master Summary — All Class Concepts at a
Glance
C++ class
Data members Member functions Constructors & Access
(fields / attributes) (methods) Destructors Specifiers
Instance Static Regular const Static Default Param
Destructor public private protected
variables variables methods methods methods ctor ctor
Inheritance
Polymorphism
(is-a) (virtual)
Figure 2 — C++ class concept taxonomy tree
Concept Keyword/syntax Purpose Example in [Link]
Class definition class Name { }; Define a new type class BankAccount { };
Data member Type name; Store object's state SuperGeometry stores
material numbers
Static data member static Type name; Shared by all instances D2Q9 stores weight factors
statically
Member function RetType Define object behaviour [Link]()
method(args)
const method RetType f() const Promise not to modify All UnitConverter methods
object are const
Static method static RetType f() Callable without an singleton::mpi()
object
public public: Accessible from All OpenLB API methods
anywhere
private private: Accessible only from Internal OpenLB data arrays
within the class
protected protected: Accessible from within IndicatorF2D _myMin,
and derived classes _myMax
OpenLB Lattice-Boltzmann | [Link]
C++ Classes — Deep Dive Study Guide | [Link] Page 41
Default constructor Class() Create object with no —
arguments
Param constructor Class(args) Create object with UnitConverter(dx, dt, L, U,
arguments nu, rho)
Init list : m1(v1), m2(v2) Initialise members UnitConverter const
efficiently in ctor members
Destructor ~Class() Automatic cleanup when SuperLattice frees GPU
object dies memory
Dot operator [Link] Access member of an [Link]()
object
Arrow operator ptr->member Access member via Internal OpenLB
pointer implementation
Inheritance : public Base Derive new class from IndicatorCircle2D : public
existing one IndicatorF2D
virtual function virtual RetType f() Enable runtime IndicatorF2D::operator() = 0
polymorphism
pure virtual =0 Force derived class to IndicatorF2D is abstract
override
override override Confirm this overrides a IndicatorCircle2D::operator()
virtual override
Abstract class has = 0 methods Cannot be instantiated IndicatorF2D — only
directly derived classes used
Functor operator()(args) Object callable like a AnalyticalConst2D,
function Poiseuille2D
this pointer this Pointer to the current Used inside OpenLB for
object chaining
— End of C++ Classes Deep Dive —
OpenLB Lattice-Boltzmann | [Link]