Select Page

C++ Programming: Dynamic or Static Polymorphism?

As the complexity of embedded software increases, fulfilling quality criteria such as modifiability, extensibility, adaptability, and reusability becomes ever more important. A key means of meeting these software quality requirements is the application of polymorphic structures in architecture, design, and implementation. Software development distinguishes between dynamic and static polymorphism. 

This article explains dynamic and static polymorphism and demonstrates their application using a simple case study. A comparison of results is presented based on three different design and implementation approaches. To fully understand this article, knowledge of... UML, assuming object-oriented programming and the C++ programming language.

Polymorphism

In the context of software development, polymorphism means the diversity of functions, i.e., functions with the same semantics and invocation in different implementations.

Polymorphism in class functions

Polymorphism in class functions

Figure 1: Polymorphism in class functions

In the context of object-oriented software development, polymorphism refers to class functions. Several classes contain one or more semantically identical functions that are implemented differently depending on the class.

The differently implemented functions are nevertheless called in the same way. Thus, a single call of the same type results in two or more variants of the function execution, also known as polymorphism (multiple forms).

Dynamic versus static polymorphism

Polymorphism in class functions with grabbers

Figure 2: Polymorphism in class functions with a grabber

When applying the dynamic polymorphism It is a runtime decision which of the possible functions is called (dynamic/late binding), and the callable function is against another as well Interchangeable at runtime.

When applying the static polymorphism It is a compile-time decision which of the possible functions is called (static / early binding), and the callable function is against another only Interchangeable at compile time.

Example: Resource access protection

The class cCounter is not thread-safe because it accesses the static member variable mStartValue within various functions.

class cCounter

Image 3: Class cCounter

In the cCounter_ThreadSafe class, which is specialized (inherited) by cCounter, resource protection for mStartValue is to be flexibly and interchangeably ensured through various operating system mechanisms (Critical Section, Mutex, later optionally Semaphore).

For classes with similar operating system mechanisms, polymorphism is a suitable approach: The classes cCriticalSection, cMutex, and cSemaphore contain semantically identical functions for requesting/locking (lock()) and unlocking the resource (unlock()). Further details regarding these resource protection mechanisms are not the focus of this article and will not be discussed further here.

Example of resource access control: Architecture and Design

Figure 4: Example of resource access protection: Architecture and design

The following section presents three specific examples. Design and implementation approaches before:

  • Interface and association
  • Template parameters
  • CRTP (Curiously Recurring Template Pattern)

In addition to the distinction between dynamic and static polymorphism, an efficiency comparison is also performed.

Interface and association

Interface and Association: Architecture and Design

Figure 5: Interface and Association: Architecture and Design

The common interface class icResourceLocker abstracts the concrete resource locker functions lock() and unlock(). In C++, these are purely virtual functions.

class icResourceLocker
{ public: virtual ~icResourceLocker() =default;
    virtual void lock(void)     =0;
    virtual void unlock(void)   =0;
};

The two resource locker classes cCriticalSection and cMutex implement the interface through inheritance and override the interface functions.

class cMutex : public icResourceLocker { public: cMutex(void) =default;     ~cMutex() override =default;
    void lock(void)   override;
    void unlock(void) override; private:
    ecLockStatus_t mStatus = ecLockStatus_t::Unlocked; };

The connection to a specific operating system has been omitted here, as it is not necessary for understanding the polymorphism. The class `ecLockStatus_t` is included as an enum class to symbolize the resource state.

void cMutex::lock(void)
{
  // ... operating system call mStatus = ecLockStatus_t::Locked;   showValue("nMutex status = locked");
}
void cMutex::unlock(void)
{
  // ... operating system call mStatus = ecLockStatus_t::Unlocked;   showValue("nMutex status = unlocked");
}

The `showValue()` output function is part of the small platform included in the examples for porting to arbitrary targets. The implementations of `cCriticalSection` and `cMutex` are essentially identical.

The cCounter__ThreadSafe class, derived from cCounter, redefines the access functions to the static attribute mStartValue of the base class with resource protection.

class cCounter_ThreadSafe : public cCounter { public: cCounter_ThreadSafe(
            icResourceLocker* const ptrResourceLocker = nullptr, uint32_t const CountValue = 0);     ~cCounter_ThreadSafe() override =default;
    void setStartValue(uint32_t const StartValue);
    uint32_t getStartValue(void);
    void setCountToStartValue(void);
    void setResourceLocker(icResourceLocker* const ptrResourceLocker); private:
      icResourceLocker* mptrResourceLocker;
};

Access to the resource locker is via the pointer `mptrResourceLocker` of the interface class `icResourceLocker`. This pointer can point to an object of type `cCriticalSection` or `cMutex` (see Liskov Substitution Principle [LSP]). The desired resource locker is selected via the constructor and/or the `cCounter_ThreadSafe::setResourceLocker()` function.

void cCounter_ThreadSafe::setCountToStartValue(void)
{ if (mptrResourceLocker != nullptr) {
    mptrResourceLocker->lock();   } cCounter::setCountToStartValue();      if (mptrResourceLocker != nullptr) {
    mptrResourceLocker->unlock();
  }
}

The example of the `cCounter_ThreadSafe::setCountToStartValue()` function demonstrates the application of the resource protection mechanism: locking, accessing, and unlocking. The `lock()` and `unlock()` functions are each called via a pointer. Therefore, the specific call/binding depends on the type of object to which the pointer is initialized. This utilizes the dynamic binding mechanism via VMT (Virtual Method Tables), illustrating the application of polymorphism.

The application instantiates Resource Locker objects and uses them to initialize Thread-Safe Counter objects.

Resource_Locker::cCriticalSection locCriticalSection{ }; Resource_Locker::cMutex locMutex{ }; Counter::cCounter_ThreadSafe locCounter_A{ &locCriticalSection }; Counter::cCounter_ThreadSafe locCounter_B{ &locMutex };

Among other things, functions that include resource protection are applicable to the counter objects.

locCounter_A.setCountToStartValue(); locCounter_B.setCountToStartValue(); locCounter_A.count(); locCounter_B.count();

The Resource Locker objects can be set using the function cCounter_ThreadSafe::setResourceLocker(). Duration to exchange.

locCounter_A.setResourceLocker(&locMutex);
locCounter_B.setResourceLocker(&locCriticalSection);

Thus, this variant corresponds to an example of the dynamic polymorphism.

Template parameters

Template Parameters: Architecture and Design

Figure 6: Template Parameters: Architecture and Design

Compared to the previous version, the Resource Locker no longer contains an interface or virtual functions.

class cMutex
{ public: cMutex(void) =default;     ~cMutex() =default;
    void lock(void);
    void unlock(void);    private: ecLockStatus_t mStatus = ecLockStatus_t::Unlocked; };

The further implementation of cMutex and cCriticalSection remains unchanged.

This variant replaces the previous interface pointer in the cCounter_ThreadSafe class of the ResourceLocker_T template parameter. The subsequent typing of this parameter selects the resource locker to be used, which in turn corresponds to polymorphism.

template<typename ResourceLocker_T>
class tcCounter_ThreadSafe : public cCounter { public: tcCounter_ThreadSafe(uint32_t const CountValue = 0);     ~tcCounter_ThreadSafe() =default;
    void setStartValue(uint32_t const StartValue);
    uint32_t getStartValue(void);
    void setCountToStartValue(void); private:
    ResourceLocker_T mResourceLocker;
};

Instead of the interface pointer, a concrete object of the template parameter type is used; thus, the otherwise required pointer queries are eliminated.

template<typename ResourceLocker_T>
void tcCounter_ThreadSafe<ResourceLocker_T>::setCountToStartValue(void)
{
  mResourceLocker.lock();   cCounter::setCountToStartValue();
  mResourceLocker.unlock();
}

In this case, the binding between object and function is static / early, i.e., at compile time.

The application instantiates objects of the template class cCounter_ThreadSafe and thus initializes the resource locker via the template parameter.

tcCounter_ThreadSafe<cCriticalSection> locCounter_CriticalSection{ };
tcCounter_ThreadSafe<cMutex> locCounter_Mutex{ };

In contrast to the previous version, here the resource lockers can only be set at coding/compilation time, but can no longer be swapped at runtime.

Thus, this variant corresponds to an example of the static polymorphism.

CRTP

Architecture and Design

Image 7: CRTP: Architecture and Design

One criticism of the template parameter approach is the lack of a common interface agreement for the resource locker classes. This criticism is eliminated by using the Curiously Recurring Template Pattern (CRTP).

template<typename ResourceLocker_T>
class ticResourceLocker
{ public: ticResourceLocker(void) =default;     ~ticResourceLocker() =default;
    void lock(void);
    void unlock(void);
};

This implementation variant also remains free of virtual functions. The interface class is now also a template class that expects the specific resource locker as a template parameter.

} template void ticResourceLocker ::lock(void)
{
  static_cast<ResourceLocker_T*>(this)->lock(); } template void ticResourceLocker ::unlock(void)
{
  static_cast<ResourceLocker_T*>(this)->unlock();
}

The functions ticResourceLocker::lock() and ticResourceLocker::unlock() each call the corresponding specialized functions in the derived classes via the template parameter.

class cMutex public ticResourceLocker<cMutex>
{ public: cMutex(void) =default;     ~cMutex() =default;
    void lock(void);
    void unlock(void);       private: ecLockStatus_t mStatus = ecLockStatus_t::Unlocked; };

In inheritance implementation, the template parameter of the base class is already specified with the type of the inheriting class (MixedIn). This is shown using the example of the class cMutex.

void cMutex::lock(void)
{
  // ... operating system call mStatus = ecLockStatus_t::Locked;   showValue("nMutex status = locked");
}

void cMutex::unlock(void)
{
  // ... operating system call mStatus = ecLockStatus_t::Unlocked;   showValue("nMutex status = unlocked");
}

The function implementations of lock() and unlock() remain unchanged. The cCounter_ThreadSafe class implementation also remains unchanged compared to the Template Parameter variant.

tcCounter_ThreadSafe locCounter_CriticalSection{ };

tcCounter_ThreadSafe locCounter_Mutex{ };

The resource locker is only set at encoding/compilation time and cannot be changed at runtime.

Thus, this variant corresponds to a second, extended example of the static polymorphism.

Comparison

This section compares all three variants (interface and association, template parameters, and CRTP). The consumption figures are based on the following setup:

  • STM32F407 microcontroller with SYSCLK = 168MHz
  • MCBSTM32F400 Evaluation Board
  • MDK-ARM v5.35.0.2 Tool Chain
  • ARM Clang Compiler v6.16
  • Compiler optimization: Default
  • C++14


Summary

From the perspective of software quality attributes such as functional safety, attack resistance, reliability, and resource consumption, implementing static polymorphism is always the better choice. The price to pay for this is the loss of the positive support for software quality attributes like modifiability, extensibility, and adaptability that dynamic polymorphism provides.

The decision between dynamic and static polymorphism doesn't have to be made digitally for the entire software architecture – hybrid approaches are conceivable. However, as always, the specific decisions depend on the specific software requirements.

Further information

MicroConsult expertise in embedded software development

MicroConsult Training & Coaching on Analysis, Design, Architecture

All training courses & dates at a glance

MicroConsult Newsletter

With the MicroConsult newsletter, you'll stay on the pulse of the embedded world. Look forward to proven practical knowledge, real professional tips, and current events – directly from our experts for your project success.

Subscribe now!

Published by

Thomas Batt

Thomas Batt