Practice makes perfect, part 2 – foundation kata

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!

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.

Glennan Carnie
Dislike (0)
Website | + posts

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.

About Glennan Carnie

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.
This entry was posted in C/C++ Programming, Design Issues, training and tagged , , , . Bookmark the permalink.

1 Response to Practice makes perfect, part 2 – foundation kata

  1. Greg says:

    Well demonstrated, thanks.
    Foundations with clarity can make (or without break) a project...

    Like (0)
    Dislike (0)

Leave a Reply