Understanding of / requirements for polymorphism
To understand polymorphism - as the term is used in Computing Science - it helps to start from a simple test for and definition of it. Consider:
Type1 x;
Type2 y;
f(x);
f(y);
Here, f()
is to perform some operation and is being given values x
and y
as inputs.
To exhibit polymorphism, f()
must be able to operate with values of at least two distinct types (e.g. int
and double
), finding and executing distinct type-appropriate code.
C++ mechanisms for polymorphism
Explicit programmer-specified polymorphism
You can write f()
such that it can operate on multiple types in any of the following ways:
Preprocessing:
#define f(X) ((X) += 2)
// (note: in real code, use a longer uppercase name for a macro!)
Overloading:
void f(int& x) { x += 2; }
void f(double& x) { x += 2; }
Templates:
template <typename T>
void f(T& x) { x += 2; }
Virtual dispatch:
struct Base { virtual Base& operator+=(int) = 0; };
struct X : Base
{
X(int n) : n_(n) { }
X& operator+=(int n) { n_ += n; return *this; }
int n_;
};
struct Y : Base
{
Y(double n) : n_(n) { }
Y& operator+=(int n) { n_ += n; return *this; }
double n_;
};
void f(Base& x) { x += 2; } // run-time polymorphic dispatch
Other related mechanisms
Compiler-provided polymorphism for builtin types, Standard conversions, and casting/coercion are discussed later for completeness as:
- they're commonly intuitively understood anyway (warranting a "oh, that" reaction),
- they impact the threshold in requiring, and seamlessness in using, the above mechanisms, and
- explanation is a fiddly distraction from more important concepts.
Terminology
Further categorisation
Given the polymorphic mechanisms above, we can categorise them in various ways:
1 - Templates are extremely flexible. SFINAE (see also std::enable_if
) effectively allows several sets of expectations for parametric polymorphism. For example, you might encode that when the type of data you're processing has a .size()
member you'll use one function, otherwise another function that doesn't need .size()
(but presumably suffers in some way - e.g. using the slower strlen()
or not printing as useful a message in the log). You can also specify ad-hoc behaviours when the template is instantiated with specific parameters, either leaving some parameters parametric (partial template specialisation) or not (full specialisation).
"Polymorphic"
Alf Steinbach comments that in the C++ Standard polymorphic only refers to run-time polymorphism using virtual dispatch. General Comp. Sci. meaning is more inclusive, as per C++ creator Bjarne Stroustrup's glossary (http://www.stroustrup.com/glossary.html):
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. TC++PL 12.2.6, 13.6.1, D&E 2.9.
This answer - like the question - relates C++ features to the Comp. Sci. terminology.
Discussion
With the C++ Standard using a narrower definition of "polymorphism" than the Comp. Sci. community, to ensure mutual understanding for your audience consider...
- using unambiguous terminology ("can we make this code reusable for other types?" or "can we use virtual dispatch?" rather than "can we make this code polymorphic?"), and/or
- clearly defining your terminology.
Still, what's crucial to being a great C++ programmer is understanding what polymorphism's really doing for you...
letting you write "algorithmic" code once and then apply it to many types of data
...and then be very aware of how different polymorphic mechanisms match your actual needs.
Run-time polymorphism suits:
- input processed by factory methods and spat out as an heterogeneous object collection handled via
Base*
s,
- implementation chosen at runtime based on config files, command line switches, UI settings etc.,
- implementation varied at runtime, such as for a state machine pattern.
When there's not a clear driver for run-time polymorphism, compile-time options are often preferable. Consider:
- the compile-what's-called aspect of templated classes is preferable to fat interfaces failing at runtime
- SFINAE
- CRTP
- optimisations (many including inlining and dead code elimination, loop unrolling, static stack-based arrays vs heap)
__FILE__
, __LINE__
, string literal concatenation and other unique capabilities of macros (which remain evil ;-))
- templates and macros test semantic usage is supported, but don't artificially restrict how that support is provided (as virtual dispatch tends to by requiring exactly matching member function overrides)
Other mechanisms supporting polymorphism
As promised, for completeness several peripheral topics are covered:
- compiler-provided overloads
- conversions
- casts/coercion
This answer concludes with a discussion of how the above combine to empower and simplify polymorphic code - especially parametric polymorphism (templates and macros).
Mechanisms for mapping to type-specific operations
> Implicit compiler-provided overloads
Conceptually, the compiler overloads many operators for builtin types. It's not conceptually different from user-specified overloading, but is listed as it's easily overlooked. For example, you can add to int
s and double
s using the same notation x += 2
and the compiler produces:
- type-specific CPU instructions
- a result of the same type.
Overloading then seamlessly extends to user-defined types:
std::string x;
int y = 0;
x += 'c';
y += 'c';
Compiler-provided overloads for basic types is common in high-level (3GL+) computer languages, and explicit discussion of polymorphism generally implies something more. (2GLs - assembly languages - often require the programmer to explicitly use different mnemonics for different types.)
> Standard conversions
The C++ Standard's fourth section describes Standard conversions.
The first point summarises nicely (from an old draft - hopefully still substantially correct):
-1- Standard conversions are implicit conversions defined for built-in types. Clause conv enumerates the full set of such conversions. A standard conversion sequence is a sequence of standard conversions in the following order:
Zero or one conversion from the following set: lvalue-to-rvalue conversion, array-to-pointer conversion, and function-to-pointer conversion.
Zero or one conversion from the following set: integral promotions, floating point promotion, integral conversions, floating point conversions, floating-integral conversions, pointer conversions, pointer to member conversions, and boolean conversions.
Zero or one qualification conversion.
[Note: a standard conversion sequence can be empty, i.e., it can consist of no conversions. ] A standard conversion sequence will be applied to an expression if necessary to convert it to a required destination type.
These conversions allow code such as:
double a(double x) { return x + 2; }
a(3.14);
a(42);
Applying the earlier test:
To be polymorphic, [a()
] must be able to operate with values of at least two distinct types (e.g. int
and double
), finding and executing type-appropriate code.
a()
itself runs code specificall