Static Polymorphism using Curiously Recurring Template Pattern (CRTP)

Written by SoulCollector | Published 2020/04/05
Tech Story Tags: c++ | object-oriented | polymorphism | static-dispatch | coding | design-patterns | cpp | programming

TLDR Polymorphism is the ability (in programming) to present the same interface for differing underlying forms (data types) in C++. C++ support both Dynamic and Static polymorphism. Dynamic polymorphism means the type of the object is not known at compile time ( maybe based on user input, etc), and thus the compiler adds extra data structure to support this. Static Polymorism can be used when one wants their own Expression Template Library. The C++ standard doesn't dictate on how it should be implemented, so we will brush aside all the discussion on dynamic polymorphism.via the TL;DR App

Inheritance is one of the most used method for code reuse. Object Oriented Languages strive on the inheritance to collect the common functionality in a base class.

What is Polymorphism?

If you think about the Greek roots of the term, it should become obvious.
Poly = many: polygon = many-sided, polystyrene = many styrenes (a), polyglot = many languages, and so on.Morph = change or form: morphology = study of biological form, Morpheus = the Greek god of dreams able to take any form.
So polymorphism is the ability (in programming) to present the same interface for differing underlying forms (data types).

Polymorphism in C++

C++ support both Dynamic and Static Polymorphism.
  • Dynamic Polymorphism : In this type of polymorphism, the type of the object is not known at compile time ( maybe based on user input, etc), and thus compiler adds extra data structure to support this. The standard doesn't dictate on how it should be implemented.
  • Static Polymorphism : In this type, the type of the object is known at the compile time itself and hence there is no need to actually save extra information in the data structure. But as mentioned in before, we need to know the type of the object at compile time.
Static Polymorphism using the Curiously Recurring Template Pattern
Since this article is about static polymorphism, we will brush aside all the discussion on dynamic polymorphism.
Let's try to simulate calling different Binary Operators. In the grand-scheme of things this can be used when one wants their own Expression Template Library.
Let's see a simple example code.
template <typename Derived>
struct BinaryOperator{
  float interface(float x, float y){
    return static_cast<Derived*>(this)->implementation(x, y);
  }
};
Here we have a base class template
BinaryOperator
(note that this is not a class as it won't instantiated will the Derived Type is known) with the interface method which calls the implementation of the derived class.
struct Add: BinaryOperator<Add>{
  float implementation(float x, float y){
    std::cout << "Implementation Add" << std::endl;
    return x + y;
  }
};

struct Subtract: BinaryOperator<Subtract>{
  float implementation(float x, float y){
    std::cout << "Implementation Sub" << std::endl;
    return x - y;
  }
};
These are our derived classes for the
BinaryOperator
. It is after looking at
BinaryOperator<Add>
that the compiler generates a specialised base class for the
Add
and similarly for
Subtract
.
So now if we create objects of our derived classes and hold a reference to them using our base class, we can call the interface. This call will dispatch to the implementation of derived class.
int main(){
  
  std::cout << std::endl;
  
  BinaryOperator<Add>&& add = Add{};
  BinaryOperator<Subtract>&& subtract = Subtract{};
  std::cout << add.interface(4, 5) << std::endl;
  std::cout << subtract.interface(4, 5) << std::endl;
  std::cout << std::endl;
  
}
As expected the calls for add.interface will be dispatched to
Add's
implementation and similarly for
Subtract
.
Implementation Add

9

Implementation Sub

-1
Here is the whole code snippet.
#include <iostream>

template <typename Derived>
struct BinaryOperator{
  float interface(float x, float y){
    return static_cast<Derived*>(this)->implementation(x, y);
  }
};

struct Add: BinaryOperator<Add>{
  float implementation(float x, float y){
    std::cout << "Implementation Add" << std::endl;
    return x + y;
  }
};

struct Subtract: BinaryOperator<Subtract>{
  float implementation(float x, float y){
    std::cout << "Implementation Sub" << std::endl;
    return x - y;
  }
};

int main(){
  
  std::cout << std::endl;
  BinaryOperator<Add>&& add = Add{};
  BinaryOperator<Subtract>&& subtract = Subtract{};
  std::cout << add.interface(4, 5) << std::endl;
  std::cout << subtract.interface(4, 5) << std::endl;
  std::cout << std::endl;
  
}
If one is not happy with explicitly working with floats, then we can also take the
DataType
as template parameter.
#include <iostream>

template <typename Derived, typename DataType>
struct BinaryOperator{
  DataType operator()(DataType x, DataType y){
    return static_cast<Derived*>(this)->implementation(x, y);
  }
};

template <typename DataType>
struct Add: BinaryOperator<Add<DataType>, DataType>{
  DataType implementation(DataType x, DataType y){
    std::cout << "Implementation Add" << std::endl;
    return x + y;
  }
};

template <typename DataType>
struct Subtract: BinaryOperator<Subtract<DataType>, DataType>{
  DataType implementation(DataType x, DataType y){
    std::cout << "Implementation Sub" << std::endl;
    return x - y;
  }
};

template <typename DType, template <typename> class Op>
DType execute(Op<DType> op, DType x, DType y){
    return op(x, y);
}

int main(){
  using DType = double;
  std::cout << std::endl;

  DType x = 4;
  DType y = 5;
  std::cout << execute(Add<DType>{}, x, y) << std::endl;
  std::cout << execute(Subtract<DType>{}, x, y) << std::endl;
  std::cout << std::endl;
  
}
Thanks for reading!

Published by HackerNoon on 2020/04/05