Static Polymorphism in C++

Kateryna Bondarenko
7 min readMay 6, 2019

--

What is polymorphism in general?

Giving a definition as simple as it theoretically possible, polymorphism is an ability to treat objects of different types as if they are of a same type. There are several types of polymorphism and different methods of achieving polymorphic behavior.

Academic definition

According to Bjarne Stroustrup, father of C++ language,

polymorphism — providing a single interface to entities of different types. virtual functions provide dynamic (run-time) polymorphism through an interface provided by a base class. Overloaded functions and templates provide static (compile-time) polymorphism.

Breaking down the Stroustrup’s definition, polymorphism can be divided in two categories:

  • Static polymorphism with overloaded functions and templates that happens at compile time;
  • Dynamic polymorphism with interfaces that happens in run-time.

Note that C++ standard defines polymorphic objects as objects whose

implementation generates information associated with each such object that makes it possible to determine that object’s type during program execution.

and also has a short definition of polymorphic class:

A class that declares or inherits a virtual function is called a polymorphic class.

which, basically, omits static (compile-time) polymorphism. However, the term “polymorphism” is used to characterize both compile-time and run-time type so often, that it is hard to avoid describing one or another.

What is static polymorphism?

Even though you can treat different types as if they are the same, you can’t fully ignore types — they still need to be resolved for your program to run. Such determination happens via different methods at different time. The key difference of static polymorphism is that polymorphic — unknown — types are resolved by compiler at compile stage. It might be hard to grasp, but examples will make understanding of the concept easier.

Overloading

Overloaded operator

Have you ever appreciated the abilities of built-in + operator? It works perfectly with numerical values, and even has a special behavior for non-numerical built-in types. You can add int, char, bool, and even all of them at once using the same + sign. Operator + is overloaded — it changes its behavior basing on argument types.

As it turns out, there are several + operator’s definitions: one designed for int, another for char, etc., and the compiler picks the appropriate version of the definition under different circumstances. It might look like you are using the same + sign over and over again although the compiler deduces arguments’ types, decides which definition of + operator suits best, and puts the chosen version of operator in your code.

Operator + is not unique. Many built-in operators are overloaded to support variety of built-in types. You can even define additional meaning for built-in operators to add support for your custom type (class). Doing so does not affect operators in general — you just add yet another definition of the operator to the pool.

Important note: Bjarne Stroustrup’s definition of static polymorphism omits overloaded operators. However, such operators are sometimes seen as a basic polymorphism feature, and they are helpful in explaining overloaded functions — true implementation of polymorphic behavior.

Overloaded function

Overloaded functions work just like an overloaded operators. If several definitions of a function with the same name are provided in the same scope (meaning they are equally accessible), compiler picks the appropriate one at the compile time judging on type and amount of given input.

Now the idea of polymorphism starts to emerge: you are using “the same interface” (the same function name) to work with objects of different types.

With the first custom_add(c, e) call, compiler checks the type of input parameters (int , int ). Compiler searches for function with signature custom_add( int , int ), and uses the one that fits best. Thereafter, the process is repeated for each overloaded function call.

Point to remember: each time you leave something for compiler to deduce — make sure there is no room for ambiguity left.

Calling function custom_add(c, r) with int c and float r is ambiguous and will result in compile time error. Compiler searches for function with signature custom_add( int , float ), and does not find one. Note, our program has two potential candidates, custom_add( float , float ) and custom_add( int , int ) with both functions equally fulfilling (or not fulfilling) the purpose. In this case, compiler can not decide on which one to use and reports the issue.

Templates

Function template

The idea of overloading functions is pretty handy when you have to do slightly different things with different data types. But what if you need to do exactly the same to similar data types? You could write several almost identical functions for each data type you want to support using function overloading. However, C++ provides a better way to approach this task — here is where function templates come to play.

Function template can be seen as a pattern for producing specialized functions fit for a particular purpose. Your responsibility is to create an algorithm, step-by-step instruction that describes what work needs to be done. Compiler’s responsibility is to generate code for different input types based on instructions you gave.

Short note: T is a very common name for a template parameter and is usually used instead of more meaningful names. However, you are free to pick the one that either is agreed upon in your working environment or makes more sense to you.

Template function custom_add(T a, T b) can be used with any type that supports all operations in all described steps. Since the algorithm is pretty basic, this function can be used with any built-in type, and with any custom data type (class) that supports + and << operators. However, calling custom_add(p, e) with int p and float e will cause a compile time error. To understand why, you need to look at the process of function creation in a step-by-step manner. Keep in mind that described process is highly simplified.

First call of custom_add<int>(p, i) invokes a creation of suitable function. Type specifier <int> spells out the input type. Compiler takes given template void custom_add (T a, T b), substitutes T with given type int resulting in custom_add( int , int ), and the process goes smoothly.

At the second call of custom_add(n, e), compiler first checks if it has made the proper function already. The one from before (custom_add( int , int )) does not quite fit to description. Compiler deduces input types (float, float), substitutes T with float, and succeeds in new function creation.

Reading the third function call custom_add( int , float ), compiler tries to substitute T with int which leads to custom_add ( int , int ) and with float resulting in custom_add ( float , float ) none of which is a match to an original call. Compiler reports the issue and aborts compilation process.

Type specifier

Putting type specifier when using a template function is not required, but is generally considered as a good practice. There is a drawback: adding type specifier custom_add<int>(p, e) will tell compiler to treat both inputs as int values (despite value e being a float) and hide the compile-time error. In this case, type of input value will be neglected which sometimes can lead to nasty hard-to-debug errors. Templates are powerful, but require some caution when using.

Class template

Think of a situation where you need a basic container which would store two values: container for two ints, two chars, two floats, etc. Instead of creating several classes (each for each data type) you can easily apply the previous approach of creating generalized code using a class template.

You still have to make sure that all instructions you are giving will be applicable to data types you are going to use them with. You don’t need to worry much while working with built-in types, but it becomes a big deal when you start using templates with custom classes.

Additionally, compiler will go through the same process of deducing T type as in the function template, so the same resolving constraints apply.

Note: Even though the class Two_values is a general purpose container that can store any data, method custom_add() limits its use. The method can be applied to types that support << and + operators which might not be the case for some custom-made classes.

Specializing templates

Function custom_add() from a previous example works perfectly for adding two numerical values, but adding two chars and getting numerical result makes little sense in given context. There is a nice handy way to have everything working as is and having an exception for char values. You can easily specialize template (function, class, or both) so it will behave differently in specific case.

Now, you have a generalized class template which can be used for creation of many containers for different types of data, and a “special case” — creating two_values instance with two char values will trigger unique behavior for custom_add() method.

Overloading vs Templates

So, which one is better? There is no universal answer to this question. They both provide polymorphic behavior during compilation process, both are useful, and both should be in your toolbox. As a general rule of thumb, use:

  • Templates when algorithm is the same for each case (possibly with few exceptional cases for some data types);
  • Overloading when algorithm is slightly different for each case;
  • None of the above when algorithm is different for each case.

Bonus topics for self-exploration:

  • DRY principle (Beginner)
  • Variadic templates (Upper-Intermediate)
  • Template Metaprogramming or compile-time execution (Upper-Intermediate)
  • The Curiously Recurring Template Pattern (CRTP) (Advanced)

--

--