In the previously article we looked at the need for repetitive practice – code kata. In this article I want to present some of my preferred foundational kata.
If you’re a beginner to C++ I recommend you fully internalize all these examples before having a look at the idiomatic kata.
If you’re a more experienced C++ programmer you may be looking at these kata and thinking “Jeez – these are so basic! Who couldn’t do this!”. Bear in mind though – we all started somewhere! I still practice most of these kata regularly. Remember, you practice exercises like this not until you get them right, but until you can’t get them wrong!
Contents
Simple class
Why is this important?
Building a simple class is the core to Object-based programming
Variations
- Overload member functions
- Add static members
- Add messages to constructor / destructor and track object lifetime
- Add const member functions
Example
class ADT { public: ADT() = default; ADT(int init); void op() const; private: int data { }; }; ADT::ADT(int init) : data { init } { } void ADT::op() const { // ... } int main() { ADT adt1 { 2 }; ADT adt2 { }; adt1.op(); }
1:1 Association
Why is this important?
The 1:1 client-server association is the most common relationship between objects
Reinforces lifetime management of objects is independent of communication between the objects.
For more information have a look at this article.
Variations
- Use the constructor as a binding function
- Use a member function as the binding function
- Use a friend function as the binding function
- Dynamically allocate client and server object with smart pointers
Example
class Server { public: void op(); }; class Client { public: void run() { server->op(); } friend void connect(Client& client, Server& server); private: Server* server { nullptr }; }; void connect(Client& client, Server& server) { client.server = &server; } int main() { Client client { }; Server server { }; connect(client, server); client.run(); }
1:N Association
Why is this important?
Very common variation of 1:1 association
Variations
- Fixed-size using std::array
- Use the constructor as a binding function with an initializer list.
- Use a member function as the binding function
- Use a friend function as the binding function
Example
class Step { public: void run() { /*... */ } }; class Programme { public: Programme(std::initializer_list<Step*> init); void execute(); void add(Step& step); private: std::vector<Step*> steps { }; }; Programme::Programme(std::initializer_list<Step*> init) : steps { init } { } void Programme::add(Step& step) { steps.push_back(&step); } void Programme::execute() { for (auto& step : steps) { step->run(); } } int main() { Step step1 { }; Step step2 { }; Step step3 { }; // Use initializer list // Programme prog { &step1, &step2 }; prog.add(step1); prog.add(step3); prog.execute(); }
1:1 bi-directional association
Why is this important?
Implements a peer-to-peer association.
The lifetime of the objects is independent of their communication.
Variations
- Use a (pair of) member functions as the binding functions
Example
(Note – the example below will cycle indefinitely; probably until you run out of stack!)
class Controller; class UI { public: void status(); friend void connect(UI& ui, Controller& ctrl); private: Controller* controller { nullptr }; }; class Controller { public: void command(); friend void connect(UI& ui, Controller& ctrl); private: UI* ui { nullptr }; }; void UI::status() { controller->command(); } void Controller::command() { ui->status(); } void connect(UI& ui, Controller& ctrl) { ui.controller = &ctrl; ctrl.ui = &ui; } int main() { UI ui { }; Controller ctrl { }; connect(ui, ctrl); ui.status(); }
Composition 1:1
Why is this important?
Composites manage the lifetime of their component parts (unlike association)
Variations
- Composite parts with non-default constructors
- Using NSDMI to initialize parts
- Overloading the constructor on the composite to initialize the parts
Example
class Sensor { public: Sensor(double gain) { } double get_value() { return 1.0; } }; class Positioner { public: Positioner() = default; Positioner(double sensor_gain); private: Sensor sensor { 1.0 }; }; Positioner::Positioner(double sensor_gain) : sensor { sensor_gain } { } int main() { Positioner pos1 { }; Positioner pos2 { 2.0 }; }
0 .. 1 composition
Why is this important?
Managing the lifetime of an object that may be shorter than that of its parent.
Also known as optional composition
Variations
- Manage lifetime of composite part via a smart pointer
Example
#include <iostream> #include <optional> class Sensor { public: Sensor(double gain) { } double read() { /*... */ } }; class Positioner { public: Positioner(bool make_sensor); void run(); private: std::optional<Sensor> sensor { std::nullopt }; }; Positioner::Positioner(bool make_sensor) { if (make_sensor) { sensor = Sensor { 1.0 }; } } void Positioner::run() { if (sensor.has_value()) { std::cout << sensor->read() << std::endl; } } int main() { Positioner pos1 { true }; Positioner pos2 { false }; pos1.run(); pos2.run(); }
Concrete inheritance
Why is this important?
Reinforces basic inheritance syntax.
Also reinforces the basic concept of substitutability
Variations
- Extend derived class interface; use dynamic cast to access extended interface
- Dynamically-allocate derived type via pointer-to-base
Example
class Base { public: Base() = default; Base(int init) { } virtual void op_A(); virtual void op_B(); private: int data; }; void Base::op_A() { // ... } void Base::op_B() { // ... } class Derived : public Base { public: using Base::Base; Derived(int i, int j); virtual void op_A() override; private: int more_data { 1 }; }; Derived::Derived(int i, int j) : Base { i } { } void Derived::op_A() { Base::op_A(); // Call base class method // ... } class Client { public: void run(); private: Base* server { nullptr }; friend void connect(Client& client, Base& base); }; void Client::run() { server->op_A(); server->op_B(); } void connect(Client& client, Base& base) { client.server = &base; } int main() { Base base { }; Derived derived { }; Client client { }; connect(client, base); client.run(); connect(client, derived); client.run(); }
Abstract Base Class
Why is this important?
Building a ‘family’ of substitutable types.
For more information, have a look at this article.
Variations
- Provide an implementation of pure virtual function(!)
- Add a protected interface
Example
class Abstract_base { public: Abstract_base() = default; Abstract_base(int init) : common_data { init } { } virtual ~Abstract_base() = default; virtual void op() = 0; protected: void common_fn(); private: int common_data { }; }; void Abstract_base::common_fn() { // ... } class Impl : public Abstract_base { public: using Abstract_base::Abstract_base; void op() override; void extension_fn(); }; void Impl::op() { // ... } void Impl::extension_fn() { Abstract_base::common_fn(); } class Client { public: void run(); private: Abstract_base* server { nullptr }; friend void connect(Client& client, Abstract_base& serv); }; void Client::run() { server->op(); // Dynamic downcast // Impl* impl = dynamic_cast<Impl*>(server); if (impl) { impl->extension_fn(); } } void connect(Client& client, Abstract_base& serv) { client.server = &serv; } int main() { Client client { }; Impl impl { 100 }; connect(client, impl); client.run(); }
Interface
Why is this important?
Represents the C++ simulation of the interface concept
Forms the basis of many design patterns
Variations
- Multiple inheritance of interface
- Cross-cast between interfaces
Example
class Interface { public: virtual ~Interface() = default; virtual void op_A() = 0; virtual void op_B() = 0; }; class Realization : public Interface { public: Realization(int init) { } protected: void op_A() override { /* ... */ } void op_B() override { /* ... */ } }; class Client { public: void run(); private: Interface* server { nullptr }; friend void connect(Client& client, Interface& serv); }; void Client::run() { server->op_A(); server->op_B(); } void connect(Client& client, Interface& serv) { client.server = &serv; } int main() { Client client { }; Realization impl { 100 }; connect(client, impl); client.run(); }
Stream operator overload
Why is this important?
The ability to stream a class is often useful
Operator overloads have a different syntax to normal member functions.
Reinforces the idea of the interface of a class being more than just member functions.
Example
#include <iostream> class Pressure { public: Pressure() = default; Pressure(double init) : value { init } { } friend std::ostream& operator<<(std::ostream& os, const Pressure& p); private: double value { 0.0 }; }; std::ostream& operator<<(std::ostream& os, const Pressure& p) { os << p.value; return os; } int main() { Pressure upstream { 200.0 }; std::cout << upstream << std::endl; }
Template function
Why is this important?
Forms the basis of generic code
Variations
- Add two or more template parameters
Example
template <typename T1, typename T2> auto min(const T1& lhs, const T2& rhs) { return (lhs < rhs) ? lhs : rhs; } int main() { int a { 100 }; double d { 176.6 }; auto b = min(a, d); }
Template class
Why is this important?
Forms the basis of generic code
Variations
- Add two or more template parameters
- Add a non-type template parameter
Example
template <typename T> class Measurement { public: Measurement() = default; Measurement(T val); T get_value() const; void set_value(T val); private: T value { }; }; template <typename T> Measurement<T>::Measurement(T val) : value { val } { } template <typename T> T Measurement<T>::get_value() const { return value; } template <typename T> void Measurement<T>::set_value(T val) { value = val; } int main() { Measurement<int> m1 { 100 }; Measurement<double> m2 { 18.8 }; }
Summary
In the next article we’ll have a look at some more idiomatic kata – that is, more complex coding patterns that are very common in C++ programming.
- Practice makes perfect, part 3 – Idiomatic kata - February 27, 2020
- Practice makes perfect, part 2– foundation kata - February 13, 2020
- Practice makes perfect, part 1 – Code kata - January 30, 2020
Glennan is an embedded systems and software engineer with over 20 years experience, mostly in high-integrity systems for the defence and aerospace industry.
He specialises in C++, UML, software modelling, Systems Engineering and process development.
Well demonstrated, thanks.
Foundations with clarity can make (or without break) a project...