Wolf - C++ Extensions

Author: thothonegan
Tags: cpp gamedev development

One of the few things that C++ doesn't really have that other languages do is the ability to add functions to a class written by someone else (known as 'closed'). Now, yes, subclassing kindof does that. But lets say you're using Obj-C, in which the basic string class is NSString* . You want the ability to return a hash of the string easily, or some other operation. You can then do:

@interface NSString(URLEncodable)

- (NSString*) sha3Hash;

@end

This says to add the function sha3Hash to the interface of NSString, inside a category called URLEncodable. By doing this, every string in your program can now use this function. Even things like constant strings!

NSString* hash = [@"Hi" sha3Hash];

So how would you do this in C++? Well, first would be creating the function outside the class. This is the 'functional' way, and tends to be how a lot of STL is laid out (ala std::sort instead of obj.sort() ).

String str = "Test";
String hash = sha3Hash (str);

I'm a fan of the more object oriented approach though, especially when nesting. So is there a way we can do that?

The second major way of doing this in C++ is via a subclass. Since you can add any functions you want in a subclass, you just make your own BetterString and add it there! Great! And you just make an easy way to construct from a regular string.

 String str = "Test";
 BetterString bStr = st; // cause we're better
 String hash = bStr.sha3Hash(); // yay nice.

Except now you've got kindof a two tier system. You have the APIs that are using/expecting String, and probably a good chunk of your stuff using BetterString, since its better. And then someone else decides to create their own EvenBetterString cause BetterString wasn't good enough. Or maybe someone had the same idea with the original string, so they made UltimateString, which isn't compatible with BetterString and now you have a mess.

The main problem with this is every extension is pretty much a 'fork' of the original class. While the forks could try to remain compatible with the base class or the other forks, you cant possibly know what other subclasses people might make. And while you could try to avoid it, chances are you'll end up storing your objects as the extension class (BetterString) so you'll have to keep track of multiple string classes.

So is there anything better we can do? As usual, Wolf has its own solution known as Extensions. I'll show it in three steps: Using an extension, writing an extension, and lastly how it works.

WolfCPlusPlus::Extension - Using

All classes which support extensions have a helper .ext() method which allows access to its extensions. This returns a reference so it cannot really be stored. So calling an extension method is as easy as:

 String str = "Test";
 String hash = str.ext().sha3Hash(); // String_SHA3Hash is the extension class we wrote to add it.

The extension can be stored temporarily to call multiple extension methods without having to ext multiple times, but its just a reference to the original string.

String str = "Test";
auto& strHashInterface = str.ext();
String hash = strHashInterface.sha3Hash();
int sizeOfHash = strHashInterface.sha3HashLength();

While its a little extra typing then languages that either have open classes (swift, rust, etc) or a true extension mechanism (objc), its not terrible. So how do you write an extension?

WolfCPlusPlus::Extension - Writing

First, the original class that you want to extend should be marked Extendable. This adds the support for ext() and acts as a safety check. So thats as easy as:

class String : public WolfCPlusPlus::Extendable
{};

Second, you write the extension. The extension just has to inherit from Extension, telling it what class its extending. It also needs to add a macro which verifies some of the rules with extensions. Generally the naming convention I use is BaseClass_ExtensionName. Extensions should almost always be final, since extending an extension doesnt make much sense.

class String_SHA3Hash final : public WolfCPlusPlus::Extension 
{
    public:
        String sha3Hash ();
};

WOLF_CPLUSPLUS_EXTENSION_VALIDATE (String_SHA3Hash);

And you're done! Include the header for the extension, and you're on your way of .ext()ing things.

WolfCPlusPlus::Extension - How it works

So how is this working? Basically its doing something similar to the subclassing trick. An extension subclasses its base class, but deletes its constructor (so you cant create them), and validates its extending a class that supports it. The base class then has the .ext() method which returns a reference to the original class, instead of creating a new object (like BetterString did). Since this is a cast though, we have to make sure our object still makes sense after the cast. This is where WOLF_CPLUSPLUS_EXTENSION_VALIDATE comes in : this macro adds some static_assert checks to make sure that you didn't change the object in a way which would make the cast fail. As such you're not allowed to add variables to your extension (where would they live?) or virtual functions (doesn't really make sense since theirs nothing to override).

Now theirs a few extra constraints in my implementation - the whole Extendable and Extension classes verify that you're only using it on objects marked as such, while the technique doesn't require this (I could extend std::string if I wanted with this, minus embedding ext as a function of std::string, it'd have to be freeform kindof like the first approach mentioned). In Wolf's case though, I wanted this more for being able to seperate classes over multiple libraries (DataRef is WolfSystem, but DataRef_SHA3 is WolfCrypto).

Due to how amazing compiler optimizations are, all of this optimizes to the same simple function call that the other approaches end up with, so theirs no extra overhead other then a little bit of typing. Making a nice way of adding functionality to classes after they've been defined.