Skip to content

Instantly share code, notes, and snippets.

@bryanedds
Last active August 10, 2020 01:56
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bryanedds/6a8ea281bb4a3d8b559e to your computer and use it in GitHub Desktop.
Save bryanedds/6a8ea281bb4a3d8b559e to your computer and use it in GitHub Desktop.
Abstract Data Types and Alternative Forms of Polymorphism in C++
#include "functional"
#include "iostream"
namespace proj
{
/// Polymorphism in C++ is typically implemented with dynamic dispatch. On the plus side, it's
/// easy to grasp (initially), well-understood, and works retroactively. On the minus side, it
/// sucks you into OOP quagmires too easily and is not terribly efficient. Knowing about other
/// types of polymorphims may lead you to avoid this approach wherever practical. Places where
/// it's practically unavoidable are those that require user-defined subtypes, such as plugins
/// and simulation components. However, these situations are much rarer than most people think.
class Polymorph
{
virtual void Op() = 0;
};
/// Here we have an Abstract Data Type. You can tell it is different from an object as it has
/// no instance functions. Its advantages are that it should keep you from using OOP
/// unnecessarily, and has better generic usage (especially with respect to the following forms
/// of polymorphism. The downside is that Adts are considered novel in some C++ shops, and
/// their adoption may therefore pose political difficulties.
///
/// Adts are your good alternative to objects and OOP generally.
class Adt
{
private:
const int value;
public:
Adt(int value) : value(value) { }
/// Here we have a static member function that accesses the Adt's private member directly.
/// This is not really an intended part of the interface, since below we'll be writing a
/// stand-alone function that forwards to it.
///
/// You'll notice that we could change this into a stand-alone friend function instead of
/// writing this plus the below forwarder. However, that approach may be less politically
/// viable since some C++ shops reject usage of friend constructs out of hand.
///
/// Either way, do whichever your shop permits.
static int func(const Adt& adt)
{
return adt.value * 5;
}
};
/// Here we write forwarding functions for out Adt's functions. Exposing the interface like
/// this will provide us the additional genericity that is accompanied by ad-hoc polymorphism
/// (AKA, operator-overloading). And for those of you wondering why we expose Adt functions as
/// stand-alone function rather than instance functions, see - http://www.gotw.ca/gotw/084.htm
int func(const Adt& adt)
{
return Adt::func(adt) * 5;
}
/// Extending an Adt is simple enough: just add a function in the same namesapce as the Adt!
int extension(const Adt& adt)
{
return func(adt) + 5;
}
/// Structural, or 'static duck-type', polymorphism is easy, useful, and usually politically
/// viable in C++ shops. However, it's not the most powerful form of static polymorphism in
/// C++, and its applicability is therefore limited.
template<typename T>
int pow(const T& t)
{
return func(t) * func(t);
}
template<typename T>
int pow_ex(const T& t)
{
return extension(t) * extension(t);
}
/// Template specialization provides a powerful form of static polymorphism in C++. On the plus
/// side, it's generally efficient, and in simpler forms, is easy to understand. On the minus,
/// once this approach starts invoking multiple different functions on the generalized type, it
/// starts becoming more like a limited form of type classes. That, in itself is quite a good
/// thing, but considering that C++ Concepts still haven't made it into the language, code that
/// leverages this form of polymorphism in complicated ways can be increasingly difficult for
/// many devs to deal with.
template<typename T>
T op(const T& a, const T& b)
{
return a + b;
}
template<>
int op(const int& a, const int& b)
{
return a * b;
}
template<>
float op(const float& a, const float& b)
{
return a / b;
}
/// Note that function templates shadow rather than overload, so if you want to have the same
/// generic function with a different number of parameters, you do as the standard library does
/// and suffix the name with the number parameters. This is the same approach as taken in most
/// functional languages. Usually the original function remains un-numbered, so you can apply
/// this after-the-fact.
template<typename T>
T op3(const T& a, const T& b, const T& c)
{
return a + b + c;
}
}
int main(int argc, const char* argv)
{
// some shops permit opening namespaces in local function scopes, so I will do that here
using namespace std;
using namespace proj;
// leveraging our abstract data type
auto adt = Adt(10);
cout << func(adt);
cout << extension(adt);
// we don't need bind for non-member functions, which is why writing the function
// forwarders as used here is easily worth doing.
auto fp = &func;
cout << fp(adt);
// leveraging structural polymorphism
cout << pow(adt);
cout << pow_ex(adt);
// leveraging static polymorphism
cout << op(3, 5);
cout << op(3.0f, 5.0f);
cout << op('3', '5');
// great success!
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment