Skip to content

Instantly share code, notes, and snippets.

@louis-langholtz
Last active December 3, 2024 22:51
Show Gist options
  • Save louis-langholtz/5da900c8333eed26641a09bea7aa5c31 to your computer and use it in GitHub Desktop.
Save louis-langholtz/5da900c8333eed26641a09bea7aa5c31 to your computer and use it in GitHub Desktop.
C++: My Love Affair With Polymorphic Value Types

shape-polymorphic-value-type

C++: My Love Affair With Polymorphic Value Types

If you're developing C++ code, especially for larger projects with longer histories, "polymorphic value types" might just be the best solution to your biggest coding frustrations!

Problem

Have you worked with code with countless derived classes? Have you had to deal with lots of pointers to data structures that in turn have pointers to other data structures? Did you find yourself feeling tired from the mental hopping around you were doing in order to understand that code? If so, you're not alone.

I can also imagine that you already knew you weren't alone after commiserating with coworkers about the code. At least this is an experience I've had when working on this sort of code and I don't believe that's uncommon.

This kind of code (a.k.a. pointer hell), in my experience, arises from:

  • Past decades when memory caching didn't provide nearly the speedups it does today. It was often faster for the CPU to access data via a pointer, than for it to copy all of the bytes of that data to a function.
  • Ongoing object oriented "is-a" thinking and resistance to favoring composition over inheritance.
  • The belief that polymorphic types - i.e. classes that declare or inherit at least one virtual function (see std::is_polymorphic) - are the only way, or are the best way, to provide polymorphic behavior.

Solution

Fortunately, there are alternatives. Alternatives, that can decrease the need for inheritance, decrease the uses of pointers, and make your code easier to understand.

C++17 brought us std::optional, std::variant, and std::any, all of which can in their own ways provide alternatives. My favorite alternative to pointer/inheritance hell however, and my inspiration for this article, is what's increasingly being called "polymorphic value types" and you won't need C++17 or newer to use it.

With all this talk of polymorphism, let's take a moment to recognize Wikipedia's definition of it in a nutshell as:

polymorphism is the provision of a single interface to entities of different types or the use of a single symbol to represent multiple different types.

This definition goes on to describe three major categories of polymorphism: ad hoc polymorphism, parametric polymorphism, and subtyping. Of these three categories, a polymorphic value type is best recognized as a form of ad hoc polymorphism - a class which "defines a common interface for an arbitrary set of individually specified types".

Practically speaking, polymorphic value types are classes that can be used like fundamental integer types while also providing polymorphic behavior. For instance:

  • Polymorphic value types can be constructed on the stack.
  • Polymorphic value types can be assigned and passed by value. They're not vulnerable to object slicing.
  • Polymorphic value types are not polymorphic types. They don't declare any virtual functions and don't inherit any. They're concrete regular types.
  • While instances of polymorphic value types can be seen as having only one value at a time, they're not limited to having only one type of value. Instead they're open to all types that have the functions or variables that satisfy their compile-time requirements.
  • Polymorphic value types provide polymorphic behavior based on the type of the value with which they've been constructed or to which they've been set. Notably, these input types don't have to share any common base type.

Internally speaking, polymorphic value types are built atop the more familiar pointer to implementation (PIMPL) idiom. They're a form of PIMPL on steroids if you will. The secret sauce that goes beyond the PIMPL idiom is basically the following:

  1. Having the pointed-to type being an abstract pure-virtual private nested class defining the functionality required of eligible types. Let's call this the concept class. As a nested class, in C++ terms, this doesn't make the polymorphic value type a polymorphic type - std::is_polymorphic_v<ThePolymorphicValueType> - stays false.
  2. Having a templated non-abstract private nested class that derives from the concept class. This takes a type as its template parameter, holds the value of this type, and overrides all of the concept class's pure virtual functions with functions that call similarly named and purposed free functions on the held value. Let's call this the model class.
  3. Providing public templated constructors and assignment operators. These are templated on the type they take as their input argument and store the argument via the PIMPL in an instance of the model class.
  4. Providing friend functions for the virtual member functions of the concept class. These provide usable access to those functions via the pointer to implementation in the polymorphic value type.

In contrast to polymorphic types that have reference semantics and require pointers to avoid object slicing, the combined PIMPL and secret sauce ingredients of polymorphic value types provide flexibility like std::any, with more semantic richness than std::optional and std::variant, all while supporting value semantics.

Example

A common example of a polymorphic value type is a shape class having a draw function. Let's call this class "shape" and call the function "draw".

We can create a definition for a shape class and draw function as shown in the image atop this article. Next, we can recognize various types that we think of as shapes, such as a circle and a rectangle, and create natural definitions for these like this:

struct circle { float radius; };

struct rectangle { float width; float height; };

Now, in order to use either of these types with our shape class, the model's draw code has to be able to find draw functions taking these types. So we add code like the following:

void draw(const circle& value) {
    // code to draw the circle
}

void draw(const rectangle& value) {
    // code to draw the rectangle
}

With these functions declared, the circle and rectangle types now satisfy the requirement of the shape class for having draw functions which take constant references to instances of these types. These shapes and functions can then be used like:

void example_usage() {
    shape my_shape = circle{1.2f};
    rectangle my_rectangle = rectangle{2.0f, 1.0f};
    draw(my_rectangle); // draws the rectangle
    draw(my_shape); // draws the circle
    my_shape = my_rectangle;
    draw(my_shape); // draws the rectangle
}

Notice how none of the classes inherit from any other classes and how neither of the variables above (my_shape or my_rectangle) is a pointer. And, when I want to, I can call draw(my_shape) as easily as calling draw(my_rectangle).

For more comprehensive examples of polymorphic value types, see the code for my PlayRho library's Shape or Joint class definitions. If you're interested in a detailed step-by-step explanation of how to develop your own, I highly recommend Sean Parent's 2017 talk Better Code: Runtime Polymorphism.

Conclusion

While I've found a higher up-front burden to developing polymorphic value types, I'm happy to say I've also found them easier to use, their value semantics easier to reason with, and their extensibility to be much more open than sub-classing. I hope you found this article thought provoking and I hope to see polymorphic value types embraced by more software developers!

My sincerest thanks go out to Sean Parent for having first introduced me to polymorphic value types in his 2013 presentation of Inheritance Is The Base Class of Evil.

@grantrostig
Copy link

Are we supposed to read the code in the black image? I can't read it easily and I can't copy paste it into an editor.

@louis-langholtz
Copy link
Author

Please post a screen shot of what the image looks like to you when you're having trouble reading it, and note what tool you're using to display it at that time. What happens if you try a different browser? Is the image still not readable?

I have noticed a problem before, where the image of code was basically impossible to read - which sounds like maybe what you're experiencing - but didn't then figure that out and that hasn't reoccurred for me since then. It was as if some environments though, in some situations, don't or can't display the image as it normally looks to me and is intended to be seen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment