Last active
August 10, 2020 01:56
-
-
Save bryanedds/6a8ea281bb4a3d8b559e to your computer and use it in GitHub Desktop.
Abstract Data Types and Alternative Forms of Polymorphism in C++
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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