c++patchrelease-managementcode-duplicationbackport

Reducing code duplication in C++: using same boilerplate snippets across slightly varying projects


A bit about my specific use case: I have a plugin that's designed to be integrated with Unreal Engine projects, and in order to demonstrate how to do this to users of the plugin, I've integrated it with one of Unreal's freely available sample games as an example. The integration is very specific to the game, as it does things like modifying the menu to allow the user to interact with my plugin easily.

However, in an ideal world I'd like to be able to:

  1. Provide integrations with the sample game across multiple different Unreal Engine versions. At a minimum this would include 3 currently existing versions of Unreal (4.24, 4.25 and 4.26), but would extend to potentially N different future versions. This essentially makes the integration code "boilerplate", as it's required for functionality in each sample game version, but doesn't vary at all across versions.
  2. Be able to maintain the bulk of this integration code from one place. I don't want to have to make identical modifications in each of the sample game integrations every time I change something, as juggling multiple parallel codebases like this is a lot of work and increases the probability of bugs.

This is almost a problem that could be solved with code patches: the integration code fits into the same functions/classes in the same files regardless of which version of the sample game I'm using. However, the contents of the sample game files themselves aren't exactly the same across engine versions, so a patch that says "insert this hunk into this file at this line" won't always get it right. There is also the theoretical possibility that a more substantial change is introduced into the sample game in future which could require me to change my integration in that case (though this hasn't happened yet - changes appear to be minimal across minor engine versions).

What is the best way to attack this problem? One particularly horrible way I can think of (but one which demonstrates the concept) would be to separate each chunk of the integration into a separate file, and then #include "Chunk1.inc", #include "Chunk2.inc", ... directly into the relevant classes and functions in each version of the sample game.

void ExistingClass::PerformOperationA()
{
    // ... existing implementation ...

    // Horrible hack:
    #include "IntegrationChunk1.inc"
}

I'm sure this would compile, but it wouldn't be easy for me to write or for the end user to read, as the code itself would be lacking all context when isolated in a fragment like this.

Another example might be to keep each portion of integration code in a separate class which is declared as a friend of the class where the code would normally live. The friend class is given a pointer to the main class, so can access any required private members, and has relevant functions that are called at the appropriate points. This sort of mimic's C#'s concept of a partial class, and would make the relationship between the original and modified code explicit as it's all pure, transparent C++.

For example:

class ExistingClass
{
    // Additional declaration of the friend integration class:
    friend class ExistingClass_Integration;
    ExistingClass_Integration m_Integration;

public:
    ExistingClass() :
        // ... existing initialisers ...
        m_Integration(this)

    void PerformOperationA()
    {
        // ... existing code ...
        
        // Additional bits that need to happen as part of the integration:
        m_Integration.PerformOperationA();
    }
};

class ExistingClass_Integration
{
    ExistingClass* m_Owner = nullptr;

public:
    ExistingClass_Integration(ExistingClass* owner) :
        m_Owner(owner)
    {
    }
    
    void PerformOperationA()
    {
        // Do some things here, potentially using m_Owner->...
    }
};

Apart from questions regarding whether this is recommended practice, there is also a readability tradeoff here. Although what the integration code modifies is well-defined and easy to follow, it's not really representative of what a real user would do to integrate with their game - they would just write the code in directly. The extra articulation is only there for the purposes of my own maintenance, which might make the integration seem more complex than it actually needs to be.

Is this the correct way to be thinking about this problem, or do I need to take a step back and try a different approach? Does anyone have recommendations?


In the spirit of giving some more specifics about what code I'm actually working with, my integration consists of things like the following:

Extra includes in existing source files

#include "Engine/GameInstance.h"
#include "Engine/NetworkDelegates.h"
#include "MyPlugin/MyClass.h" // <- NEW

Augmentations of existing classes

class ExistingClass
{
public:
    void Function1();
    void Function2();
    
private:
    int m_Var1;
    int m_Var2;
    
// BEGIN INTEGRATION
public:
    void ExtraFunction();
    
private:
    int m_ExtraVar;
// END INTEGRATION
};

Hooking into existing functions

The integration here is designed to simply call a new function and return, to minimise differences made to the existing code.

void ExistingClass::CreateMainMenu()
{
    // Existing implementation, eg.:
    MainMenuUI = MakeShareable(new FShooterMainMenu());
    MainMenuUI->Construct(this, Player);
    MainMenuUI->AddMenuToGameViewport();

    SetUpExtrasAfterMenuCreated() // <- NEW

    // ... further existing implementation ...
}

Solution

  • juggling multiple parallel codebases like this is a lot of work and increases the probability of bugs. .. What is the best way to attack this problem?

    There is no best way. General-purpose patching requires manual work and in some companies there are full-time employees dedicated to this. That is why having several supported releases of any product (software or anything else, really) takes a lot of money.

    The best approach is to write your software in a way that minimizes the cost of supporting old releases. Frequently, that means minimizing the cost of testing and validating old releases, rather than having automated patching which is in many instances not possible at all. Sometimes one may have better luck if one can modify the base code to make it as easy as possible to patch, but that doesn't seem to be your case.

    Even if some subset of cases could be automated, sometimes it doesn't even make sense to do it for many reasons. Some of them you already stated: it may not work on future releases, it may not be guaranteed to be reliable, users are not expecting such code, etc.

    TL;DR: backporting and maintaining several branches of software isn't cheap.